Recursive rendering in React: Building a universal JSON renderer

baso53

Sebastijan Grabar

Posted on October 14, 2018

Recursive rendering in React: Building a universal JSON renderer

In the last year, I have been working with React extensively, both for work and for my own satisfaction. I stumbled upon some pretty interesting patterns, but never did I see recursion being used in rendering React components. If you have any kind of computer science education, you were probably taught what recursion is pretty early on. What recursion does, essentially, is calling the exact same function it is currently in, until the argument is some base case which we define.

Recently, I had a task to create a React component which will render some JSON that is obtained through an API. If you are a web developer, you are probably aware that you can never be 100% sure what an API will return, and if you don't think that is the case, I recommend that you think about it, maybe you will change your mind. The web just isn't statically typed. There is one thing we will assume in this article, the API will always return some kind of JSON (or nothing).

The JSON I needed to render was an ugly, nested one, with multiple hierarchy levels. You could never know if those levels will be populated or not, will they be an empty array or null, etc. The naive way would be to create a component for every hierarchy level, and after a few minutes of being in a mindset of "oh, not again", I had an idea to recursively render those levels, exciting! As a computer programmer, I absolutely love recursion and fun ways to solve these kinds of problems.

The General Problem

Creating the component

The dummy JSON which we will be testing is generated with JSON Generator. As you can see, it has null values, empty arrays, empty objects, arrays with null values and objects with null values. Our max depth is 4 levels.



const testJson = {
  "_id": "5bc32f3f5fbd8ad01f8265fd",
  "index": 0,
  "guid": "87cfbb5d-71fb-45a7-b268-1df181da901c",
  "isActive": true,
  "balance": "$3,583.12",
  "picture": "http://placehold.it/32x32",
  "age": 31,
  "eyeColor": "brown",
  "nullTestValue": null,
  "arrayWithNulls": [null, null, null],
  "objectWithNulls": {
     "firstNullValue": null,
     "secondNullValue": null     
  },
  "name": "Becky Vega",
  "gender": "female",
  "company": "ZOID",
  "email": "beckyvega@zoid.com",
  "phone": "+1 (957) 480-3973",
  "address": "426 Hamilton Avenue, Holtville, New Hampshire, 3431",
  "about": "Duis do occaecat commodo velit exercitation aliquip mollit ad reprehenderit non cupidatat dolore ea nulla. Adipisicing ea voluptate qui sunt non culpa labore reprehenderit qui non. Eiusmod ad do in quis cillum sint pariatur. Non laboris ullamco ea voluptate et anim qui quis id exercitation mollit ullamco dolor incididunt. Ad consequat anim velit culpa. Culpa Lorem eiusmod cupidatat dolore aute quis sint ipsum. Proident voluptate occaecat nostrud officia.\r\n",
  "registered": "2016-11-19T01:14:28 -01:00",
  "latitude": -80.66618,
  "longitude": 65.090852,
  "tags": [
    "ea",
    "officia",
    "fugiat",
    "anim",
    "consequat",
    "incididunt",
    "est"
  ],
  "friends": [
    {
      "id": 0,
      "name": "Genevieve Cooke",
      "ownFriends": {
         "1": "Rebbeca",
         "2": "Julia",
         "3": "Chopper only"
      },
    },
    {
      "id": 1,
      "name": "Eaton Buck"
    },
    {
      "id": 2,
      "name": "Darla Cash"
    }
  ],
  "greeting": "Hello, Becky Vega! You have 8 unread messages.",
  "favoriteFruit": "strawberry"
}


Enter fullscreen mode Exit fullscreen mode

We will start by creating a new React project with TypeScript (because who doesn't like static typing?).



yarn create react-app recursive-component --scripts-version=react-scripts-ts


Enter fullscreen mode Exit fullscreen mode

Next, we can create a new React component to render our JSON. We can call it RecursiveProperty. The reason is that it will render a single JSON property and its value when it reaches the base case.

Our component and file structure would look like this.



import * as React from 'react';

interface IterableObject {
  [s: number]: number | string | boolean | IterableObject;
}

interface Props {
  property: number | string | boolean | IterableObject;
  propertyName: string;
  rootProperty?: boolean;
  excludeBottomBorder: boolean;
}

const RecursiveProperty: React.SFC<Props> = props => {

  return(
    <div>Our future component</div>
  );
}

export default RecursiveProperty;


Enter fullscreen mode Exit fullscreen mode

File Structure

We can now render this component in App.tsx.



import * as React from 'react';
import './App.css';

import logo from './logo.svg';
import RecursiveProperty from './RecursiveProperty';

class App extends React.Component {
  public render() {
    return (
      <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <h1 className="App-title">Welcome to React</h1>
        </header>
        <div className="App-intro">
          <RecursiveProperty property={testJson} propertyName="Root Property" excludeBottomBorder={false} rootProperty={true}/>
        </div>
      </div>
    );
  }
}

export default App;

const testJson = ...


Enter fullscreen mode Exit fullscreen mode

I removed text-align: center from App.css and added margin: 0 auto and width: 60% to .App-intro class to nicely center our list.

Next, we need to write our conditions. The component must check if the property is a leaf (the last node in the hierarchy tree). If it is, it will render the property name and its value. If not, it will recursively call the component again with the next hierarchy level passed as the property.

We will create a container for each property so we can add a little styling (using styled-components). The container will have a left margin set so that each hierarchy level is a little more indent than the previous.

In this case, we will only try to render the properties which are the leaves. With our JSON, it will render only "It isn't a leaf", because the root of a JSON document is an iterable object, which we will handle later. We know that the leaves must always be one of the three basic JSON types - boolean, string or number.



import * as React from 'react';
import styled from 'styled-components';

interface IterableObject {
  [s: string]: number | string | boolean | IterableObject;
}

interface Props {
  property: number | string | boolean | IterableObject;
  propertyName: string;
  rootProperty?: boolean;
  excludeBottomBorder: boolean;
}

const RecursivePropertyContainer = styled.div`
  padding-top: 10px;
  padding-left: 3px;
  margin-left: 10px;
  ${(props: { excludeBottomBorder: boolean }) =>
    props.excludeBottomBorder ? '' : 'border-bottom: 1px solid #b2d6ff;'}
  color: #666;    
  font-size: 16px;
`;

export const PropertyName = styled.span`
  color: black;
  font-size: 14px;
  font-weight: bold;
`;

const RecursiveProperty: React.SFC<Props> = props => {
  return (
    <RecursivePropertyContainer excludeBottomBorder={props.excludeBottomBorder}>
      {props.property ? (
        typeof props.property === 'number' ||
        typeof props.property === 'string' ||
        typeof props.property === 'boolean' ? (
          <React.Fragment>
            <PropertyName>{camelCaseToNormal(props.propertyName)}: </PropertyName>
            {props.property.toString()}
          </React.Fragment>
        ) : (
          "It isn't a leaf"
        )
      ) : (
        'Property is empty'
      )}
    </RecursivePropertyContainer>
  );
};

const camelCaseToNormal = (str: string) => str.replace(/([A-Z])/g, ' $1').replace(/^./, str2 => str2.toUpperCase());

export default RecursiveProperty;


Enter fullscreen mode Exit fullscreen mode

The camelCaseToNormal method is pretty self-explanatory, it converts camel case text to normal text with spaces.

Next, we need to recursively call the component again with the next level. We have two ways to represent a list of data in JSON - an Array of objects, or an iterable object with key/value pairs. For both cases, we need to map the properties to a new RecursiveProperty.

If we have an iterable object, we will use Object.values() method to get us an array of values (it is an ES7 method, so be sure to include it in the lib property in tsconfig.json). For passing the property names to the children we will make use of Object.getOwnPropertyNames() method. It returns an array of property names and we can make a safe access to the specific name with the index provided by the .map() method. What's great about this method is that it also works with arrays, returning the indexes instead of property keys.

Our component return() would now look like this.



return (
  <RecursivePropertyContainer excludeBottomBorder={props.excludeBottomBorder}>
    {props.property ? (
      typeof props.property === 'number' ||
      typeof props.property === 'string' ||
      typeof props.property === 'boolean' ? (
        <React.Fragment>
          <PropertyName>{camelCaseToNormal(props.propertyName)}: </PropertyName>
          {props.property.toString()}
        </React.Fragment>
      ) : (
        Object.values(props.property).map((property, index, { length }) => (
          <RecursiveProperty
            key={index}
            property={property}
            propertyName={Object.getOwnPropertyNames(props.property)[index]}
            excludeBottomBorder={index === length - 1}
          />
        ))
      )
    ) : (
      'Property is empty'
    )}
  </RecursivePropertyContainer>
);


Enter fullscreen mode Exit fullscreen mode

Now it would be great if we could collapse and expand the nested objects and only display the leaf values for the initial render.

We can make a new component for that called ExpandableProperty.



import * as React from 'react';
import styled from 'styled-components';

export const PropertyName = styled.div`
  color: #008080;
  font-size: 14px;
  font-weight: bold;
  cursor: pointer;
`;

interface Props {
  title: string;
  expanded?: boolean;
}

interface State {
  isOpen: boolean;
}

export default class ExpandableProperty extends React.Component<Props, State> {
  state = {
    isOpen: !!this.props.expanded
  };

  render() {
    return (
      <React.Fragment>
        <PropertyName onClick={() => this.setState({ isOpen: !this.state.isOpen })}>
          {this.props.title}
          {this.state.isOpen ? '-' : '+'}
        </PropertyName>
        {this.state.isOpen ? this.props.children : null}
        {React.Children.count(this.props.children) === 0 && this.state.isOpen ? 'The list is empty!' : null}
      </React.Fragment>
    );
  }
}


Enter fullscreen mode Exit fullscreen mode

We can now wrap our .map() method in this component so it can be expanded when desired.



return (
  <RecursivePropertyContainer excludeBottomBorder={props.excludeBottomBorder}>
    {props.property ? (
      typeof props.property === 'number' ||
      typeof props.property === 'string' ||
      typeof props.property === 'boolean' ? (
        <React.Fragment>
          <PropertyName>{camelCaseToNormal(props.propertyName)}: </PropertyName>
          {props.property.toString()}
        </React.Fragment>
      ) : (
        <ExpandableProperty title={camelCaseToNormal(props.propertyName)} expanded={!!props.rootProperty}>
          {Object.values(props.property).map((property, index, { length }) => (
            <RecursiveProperty
              key={index}
              property={property}
              propertyName={Object.getOwnPropertyNames(props.property)[index]}
              excludeBottomBorder={index === length - 1}
            />
          ))}
        </ExpandableProperty>
      )
    ) : (
      'Property is empty'
    )}
  </RecursivePropertyContainer>
);


Enter fullscreen mode Exit fullscreen mode

Finally, we can see it in action!

Finalized list

Voilà, we just made something useful! Recursion works pretty well with React and it is a great tool which I will surely make more use of in the future. I hope I encouraged you too to use it as well, it doesn't bite!

You can find the source code at react-recursive-component

Cheers!

💖 💪 🙅 🚩
baso53
Sebastijan Grabar

Posted on October 14, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related