TreeView component - a practical example

In this article I will describe a practical example of when I created a tree view component for this web site - using GraphQL to query data from Gatsby, JavaScript to transform the data structure into another data structure to be used in the tre view component.

#Getting data from Gatsby When building with Gatsby, we access our data through a query language - GraphQL. GraphQL allows you to declarative express your data needs. This is done with queries, queries are the representation of the data you need.

For example, I'm using static query hooks to get the data from Gatsby

/**
 * Function that query article data,
 * - filtering to include only articles that are published
 * - sorting articles in descending order
 * - format the date (published date) as long month and year only
 * @return [Array] edges - array of objects
 */
export default function useArticlesByMonth() {
  const {
    allMarkdownRemark: { edges }
  } = useStaticQuery(graphql`
    query allArticlesByMonth {
      allMarkdownRemark(
        filter: {
          fileAbsolutePath: { regex: "/(articles)/.*\\\\.md$/" }
          frontmatter: { published: { ne: "No" } }
        }
        sort: { fields: [frontmatter___date], order: DESC }
      ) {
        edges {
          node {
            frontmatter {
              title
              date(formatString: "MMMM, YYYY")
            }
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  return edges
}

The data structure this query returns have a shape typical for GraphQL and Gatsby and is described next.

/**
 * Data structure returned by GraphQL query
 */
const edges = [
  {
    node: {
      frontmatter: {
        title: 'Node.js worker threads',
        date: 'November, 2019'      },
      fields: {
        slug: '/articles/nodejs-worker-threads/'
      }
    },
    node: {
      frontmatter: {
        title: 'JavaScript prototype',
        date: 'November, 2019'      },
      fields: {
        slug: '/articles/js-object-explained/'
      }
    },
    node: {
      frontmatter: {
        title: 'Node.js architecture - overview',
        date: 'January, 2018'      },
      fields: {
        slug: '/articles/nodejs-architecture-overview/'
      }
    }
  }
]

The shape of the data structure is basically an array with objects and it is all nice and dandy, but pay attention to row number 9, 18 and 27.

When I'm creating the tree view component, the data structure will be different and we are going to need an object with keys for all date properties and array of objects for all titles and slugs for that key.

#The tree view component The tree view component is build using Material-UI's own TreeView and TreeItem components. When rendering the tre views we want to traverse a data structure that fits our requirements. The desired data structure is;

/**
 * Data structure needed to render the tree view component
 */
const articles = {
  November, 2019: [
    {
      title: 'Node.js worker threads',
      slug: '/articles/nodejs-worker-threads/'
    },
    {
      title: 'JavaScript prototype',
      slug: '/articles/js-object-explained/'
    }
  ],
  January, 2018: [
    {
      title: 'Node.js architecture - overview',
      slug: '/articles/nodejs-architecture-overview/'
    }
  ]
}

A common pattern when transforming array of objects is using the reduce method of an array and transform the array to

another data structure. For example.

/**
 * Simple array of objects data structure
 */
const people = [
  { firstname: 'Kalle', lastName: 'Kula', country: 'Sweden' },
  { firstname: 'Arne', lastName: 'Arneson', country: 'Finland' },
  { firstname: 'Kajsa', lastName: 'Andersson', country: 'Sweden' }
]

/**
 * Function that transform an array into an object by a property
 * @param {Array} objectArray - array to be transformed
 * @param {String} property - the property to use for key(s) 
 * @returns {*}
 */
function groupByProperty(objectArray, property) {
  return objectArray.reduce(function (acc, obj) {    const key = obj[property];
    if (!acc[key]) {
      acc[key] = [];
    }
    acc[key].push(obj);
    return acc;
  }, {});
}

// Transform the array into an object keyed by the country property
const groupedPeople = groupByProperty(people, 'country');

/*
 The variable groupedPeople will contain.

{
  Sweden: [
    { firstname: 'Kalle', lastName: 'Kula', country: 'Sweden' },
    { firstname: 'Kajsa', lastName: 'Andersson', country: 'Sweden' }
  ],
  Finland: [ 
    { firstname: 'Arne', lastName: 'Arneson', country: 'Finland' } 
  ]
}
*/

#The solution

I did solve this in a similar way, however, my implementation is little different since the the original array of object is nested and the solution is shown below, combined with my GraphQL query function.

/**
 * Function that groups an array of object
 * @param edges
 * @returns {*}
 */
export function groupByDate(edges) {
  return edges.reduce((acc, edge) => {
    // Destruct current object to get the property
    // I want use as a key
    const { node: { frontmatter: { date } } } = edge
    
    // In this case, assign the date 
    // property as a key
    const key = date

    if (!acc[key]) {
      acc[key] = []
    }

    // Store common objects in an array for the key
    acc[key].push({ ...edge.node.fields, ...edge.node.frontmatter })

    return acc
  }, {})
}

/**
 * Function that query article data,
 * - filtering to include only articles that are published
 * - sorting articles in descending order
 * - format the date (published date) as long month and year only
 * @return [Array] edges - array of markdown edges
 */
export default function useArticlesByMonth() {
  const {
    allMarkdownRemark: { edges }
  } = useStaticQuery(graphql`
    query allArticlesByMonth {
      allMarkdownRemark(
        filter: {
          fileAbsolutePath: { regex: "/(articles)/.*\\\\.md$/" }
          frontmatter: { published: { ne: "No" } }
        }
        sort: { fields: [frontmatter___date], order: DESC }
      ) {
        edges {
          node {
            frontmatter {
              title
              date(formatString: "MMMM, YYYY")
            }
            fields {
              slug
            }
          }
        }
      }
    }
  `)
  return groupByDate(edges)}

That's it, all the magics happen in line 60, I'm using the useArticlesByMonth function to get the correct data and structure to the tree view component. In the tree view component I iterate all keys (months) and render TreeItems in a parent children relations

The React tree view component.

import React from 'react'

// material-ui
import Paper from '@material-ui/core/Paper'
import Box from '@material-ui/core/Box'
import Typography from '@material-ui/core/Typography'
import TreeView from '@material-ui/lab/TreeView'
import TreeItem from '@material-ui/lab/TreeItem'
import ExpandMoreIcon from '@material-ui/icons/ExpandMore'
import ChevronRightIcon from '@material-ui/icons/ChevronRight'
import { withStyles } from '@material-ui/core/styles'

// Data query function
import useArticlesByMonth from '../../data/useArticlesByMonth'

const styles = theme => ({
  root: {
    flexGrow: 1,
    maxWidth: 400
  },
})

/**
 * Function render articles by month and year in a tre views
 * @param classes
 * @param navigate
 * @returns {*}
 * @constructor
 */
function ArticlesTreeView({ classes, navigate }) {
  const articlesByMonthTree = useArticlesByMonth()

  return (
    <Box clone p={1}>
      <Paper elevation={1}>
        <Typography component="div" variant="caption" gutterBottom>
          Articles by months
        </Typography>
        <TreeView
          className={classes.root}
          defaultCollapseIcon={<ExpandMoreIcon />}
          defaultExpandIcon={<ChevronRightIcon />}
        >
          {Object.keys(articlesByMonthTree).map((month, index) => {
            const nodes = articlesByMonthTree[month]

            return (
              <TreeItem key={index} nodeId={month} label={month}>
                {nodes.map(({ title, slug }) => {
                  return <TreeItem 
                           key={slug} 
                           nodeId={slug} 
                           label={title} 
                           onClick={() => navigate(slug)} />
                })}
              </TreeItem>
            )
          })}
        </TreeView>
      </Paper>
    </Box>
  )
}

export default withStyles(styles)(ArticlesTreeView)

Published: 2019-11-18
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB