Recursion In React
Tags: react, javascript
Published: 2021-09-10 07:27:57

Recursion is a powerful beast. Nothing satisfies me more than solving a problem with a recursive function that works seamlessly.

In this article I will present a simple use case to put your recursion skills to work when building out a nested Sidenav React Component.

## Setting Up

I am using React version `17.0.2`

First off, let's get a boilerplate React App going. Make sure you have Nodejs installed on your machine, then type:

`npx create-react-app sidenav-recursion`

in your terminal, in your chosen directory.
Once done, open in your editor of choice:

`cd sidenav-recursion`
`code .`

Let's install [Styled Components](https://styled-components.com), which I'll use to inject css and make it look lovely. I also very much like the [Carbon Components React](https://www.carbondesignsystem.com/guidelines/icons/library/) icons library.

`yard add styled-components @carbon/icons-react`

and finally, `yarn start` to open in your browser.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m4z9nwlocgxww7yj2is1.png)

Ok, let's make this app our own!

First, I like to wipe out everything in App.css and replace with:

```css
* {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}
```

Then I add a file in `src` called `styles.js` and start with this code:

```javascript
import styled, { css } from "styled-components";

const Body = styled.div`
  width: 100vw;
  height: 100vh;
  display: grid;
  grid-template-columns: 15% 85%;
  grid-template-rows: auto 1fr;
  grid-template-areas:
    "header header"
    "sidenav content";
`;

const Header = styled.div`
  background: darkcyan;
  color: white;
  grid-area: header;
  height: 60px;
  display: flex;
  align-items: center;
  padding: 0.5rem;
`;

const SideNav = styled.div`
  grid-area: sidenav;
  background: #eeeeee;
  width: 100%;
  height: 100%;
  padding: 1rem;
`;

const Content = styled.div`
  grid-area: content;
  width: 100%;
  height: 100%;
  padding: 1rem;
`;

export { Body, Content, Header, SideNav };
```

and then set up App.js like this:

```javascript
import "./App.css";
import { Body, Header, Content, SideNav } from "./styles";

function App() {
  return (
    <Body>
      <Header>
        <h3>My Cool App</h3>
      </Header>
      <SideNav>This is where the sidenav goes</SideNav>
      <Content>Put content here</Content>
    </Body>
  );
}

export default App;
```
And you should have something like this:
![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4362xymlf342i49ce1zz.png)

Well done for getting this far! Now for the fun stuff.
First, we need a list of sidenav options, so lets write some in a new file, `sidenavOptions.js`:
```javascript
const sidenavOptions = {
  posts: {
    title: "Posts",
    sub: {
      authors: {
        title: "Authors",
        sub: {
          message: {
            title: "Message",
          },
          view: {
            title: "View",
          },
        },
      },
      create: {
        title: "Create",
      },
      view: {
        title: "View",
      },
    },
  },
  users: {
    title: "Users",
  },
};

export default sidenavOptions;
```
Each object will have a title and optional nested paths. You can nest as much as you like, but try not go more than 4 or 5, for the users' sakes!

I then built my Menu Option style and added it to `styles.js`

```javascript
const MenuOption = styled.div`
  width: 100%;
  height: 2rem;
  background: #ddd;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: ${({ level }) => `0 ${0.5 * (level + 1)}rem`};
  cursor: pointer;
  :hover {
    background: #bbb;
  }

  ${({ isTop }) =>
    isTop &&
    css`
      background: #ccc;
      :not(:first-child) {
        margin-top: 0.2rem;
      }
    `}
`;
```

and imported it accordingly. Those string literal functions I have there allow me to pass props through the React Component and use directly in my Styled Component. You will see how this works later on.

## The Recursive Function

I then imported sidenavOptions to App.js and began to write the recursive function within the App.js component:

```javascript
import { Fragment } from "react";
import "./App.css";
import sidenavOptions from "./sidenavOptions";
import { Body, Content, Header, SideNav, Top } from "./styles";

function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      return (
        <Fragment>
          <MenuOption
            isTop={level === 0}
            level={level}
            onClick={() =>
              setOpenOptions((prev) =>
                isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
              )
            }
          >
            {option.title}
            {caret}
          </MenuOption>
          {isOpen && sub && generateSideNav(sub, level + 1)}
        </Fragment>
      );
    });
  };
  return (
    <Body>
      <Header>
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>Put content here</Content>
    </Body>
  );
}

export default App;
```

Let's slowly digest what's going on here.
first, I create a state that allows me to control which options I have clicked and are "open". This is if I have drilled down into a menu option on a deeper level. I would like the higher levels to stay open as I drill down further.

Next, I am mapping through each value in the initial object and assigning a unique (by design) openId to the option. 

I destructure the `sub` property of the option, if it exists, make a variable to track whether the given option is open or not, and finally a variable to display a caret if the option can be drilled down or not.

The component I return is wrapped in a Fragment because I want to return the menu option itself and any open submenus, if applicable, as sibling elements.

The `isTop` prop gives the component slightly different styling if it's the highest level on the sidenav.
The `level` prop gives a padding to the element which increases slightly as the level rises.
When the option is clicked, the menu option opens or closes, depending on its current state and if it has any submenus.
Finally, the recursive step! First I check that the given option has been clicked open, and it has submenus, and then I merely call the function again, now with the `sub` as the main option and the level 1 higher. Javascript does the rest!

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9n0yk77hudq06v087uqz.png)

You should have this, hopefully, by this point. 

## Let's add routing!
I guess the sidenav component is relatively useless unless each option actually points to something, so let's set that up. We will also use a recursive function to check that this specific option and its parent tree is the active link.
First, let's add the [React Router](https://reactrouter.com) package we need:

`yarn add react-router-dom`

To access all the routing functionality, we need to update our `index.js` file to wrap everything in a `BrowserRouter` component:

```javascript
import React from "react";
import ReactDOM from "react-dom";
import { BrowserRouter as Router } from "react-router-dom";
import App from "./App";
import "./index.css";
import reportWebVitals from "./reportWebVitals";

ReactDOM.render(
  <React.StrictMode>
    <Router>
      <App />
    </Router>
  </React.StrictMode>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
```

Now we need to update our sideNavOptions to include links. I also like to house all routes in my project in a single config, so I never hard-code a route. This is what my updated sidenavOptions.js looks like:

```javascript
const routes = {
  createPost: "/posts/create",
  viewPosts: "/posts/view",
  messageAuthor: "/posts/authors/message",
  viewAuthor: "/posts/authors/view",
  users: "/users",
};

const sidenavOptions = {
  posts: {
    title: "Posts",
    sub: {
      authors: {
        title: "Authors",
        sub: {
          message: {
            title: "Message",
            link: routes.messageAuthor,
          },
          view: {
            title: "View",
            link: routes.viewAuthor,
          },
        },
      },
      create: {
        title: "Create",
        link: routes.createPost,
      },
      view: {
        title: "View",
        link: routes.viewPosts,
      },
    },
  },
  users: {
    title: "Users",
    link: routes.users,
  },
};

export { sidenavOptions, routes };
```

Notice I don't have a default export anymore. I will have to modify the import statement in App.js to fix the issue.

```javascript
import {sidenavOptions, routes} from "./sidenavOptions";
```

In my `styles.js`, I added a definite color to my MenuOption component:

`color: #333;`

and updated my recursive function to wrap the MenuOption in a Link component, as well as adding basic Routing to the body. My full App.js:

```javascript
import { CaretDown20, CaretRight20 } from "@carbon/icons-react";
import { Fragment, useState } from "react";
import { Link, Route, Switch } from "react-router-dom";
import "./App.css";
import { routes, sidenavOptions } from "./sidenavOptions";
import { Body, Content, Header, MenuOption, SideNav } from "./styles";

function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub, link } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      const LinkComponent = link ? Link : Fragment;
      return (
        <Fragment>
          <LinkComponent to={link} style={{ textDecoration: "none" }}>
            <MenuOption
              isTop={level === 0}
              level={level}
              onClick={() =>
                setOpenOptions((prev) =>
                  isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
                )
              }
            >
              {option.title}
              {caret}
            </MenuOption>
          </LinkComponent>
          {isOpen && sub && generateSideNav(sub, level + 1)}
        </Fragment>
      );
    });
  };
  return (
    <Body>
      <Header>
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>
        <Switch>
          <Route
            path={routes.messageAuthor}
            render={() => <div>Message Author!</div>}
          />
          <Route
            path={routes.viewAuthor}
            render={() => <div>View Author!</div>}
          />
          <Route
            path={routes.viewPosts}
            render={() => <div>View Posts!</div>}
          />
          <Route
            path={routes.createPost}
            render={() => <div>Create Post!</div>}
          />
          <Route path={routes.users} render={() => <div>View Users!</div>} />
          <Route render={() => <div>Home Page!</div>} />
        </Switch>
      </Content>
    </Body>
  );
}

export default App;
```

So now, the routing should be all set up and working.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdjyno1ma39drjsjn3kc.png)

The last piece of the puzzle is to determine if the link is active and add some styling. The trick here is not only to determine the Menu Option of the link itself, but to ensure the styling of the entire tree is affected so that if a user refreshes the page and all the menus are collapsed, the user will still know which tree holds the active, nested link.

Firstly, I will update my MenuOption component in `styles.js` to allow for an isActive prop:

```javascript
const MenuOption = styled.div`
  color: #333;
  width: 100%;
  height: 2rem;
  background: #ddd;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: ${({ level }) => `0 ${0.5 * (level + 1)}rem`};
  cursor: pointer;
  :hover {
    background: #bbb;
  }

  ${({ isTop }) =>
    isTop &&
    css`
      background: #ccc;
      :not(:first-child) {
        margin-top: 0.2rem;
      }
    `}

  ${({ isActive }) =>
    isActive &&
    css`
      border-left: 5px solid #333;
    `}
`;
```

And my final App.js:

```javascript
import { CaretDown20, CaretRight20 } from "@carbon/icons-react";
import { Fragment, useCallback, useState } from "react";
import { Link, Route, Switch, useLocation } from "react-router-dom";
import "./App.css";
import { routes, sidenavOptions } from "./sidenavOptions";
import { Body, Content, Header, MenuOption, SideNav } from "./styles";

function App() {
  const [openOptions, setOpenOptions] = useState([]);
  const { pathname } = useLocation();

  const determineActive = useCallback(
    (option) => {
      const { sub, link } = option;
      if (sub) {
        return Object.values(sub).some((o) => determineActive(o));
      }
      return link === pathname;
    },
    [pathname]
  );

  const generateSideNav = (options, level = 0) => {
    return Object.values(options).map((option, index) => {
      const openId = `${level}.${index}`;
      const { sub, link } = option;
      const isOpen = openOptions.includes(openId);
      const caret = sub && (isOpen ? <CaretDown20 /> : <CaretRight20 />);
      const LinkComponent = link ? Link : Fragment;
      return (
        <Fragment>
          <LinkComponent to={link} style={{ textDecoration: "none" }}>
            <MenuOption
              isActive={determineActive(option)}
              isTop={level === 0}
              level={level}
              onClick={() =>
                setOpenOptions((prev) =>
                  isOpen ? prev.filter((i) => i !== openId) : [...prev, openId]
                )
              }
            >
              {option.title}
              {caret}
            </MenuOption>
          </LinkComponent>
          {isOpen && sub && generateSideNav(sub, level + 1)}
        </Fragment>
      );
    });
  };

  return (
    <Body>
      <Header>
        <h3>My Cool App</h3>
      </Header>
      <SideNav>{generateSideNav(sidenavOptions)}</SideNav>
      <Content>
        <Switch>
          <Route
            path={routes.messageAuthor}
            render={() => <div>Message Author!</div>}
          />
          <Route
            path={routes.viewAuthor}
            render={() => <div>View Author!</div>}
          />
          <Route
            path={routes.viewPosts}
            render={() => <div>View Posts!</div>}
          />
          <Route
            path={routes.createPost}
            render={() => <div>Create Post!</div>}
          />
          <Route path={routes.users} render={() => <div>View Users!</div>} />
          <Route render={() => <div>Home Page!</div>} />
        </Switch>
      </Content>
    </Body>
  );
}

export default App;

```

I am getting the current `pathname` from the `useLocation` hook in React Router. I then declare a `useCallback` function that only updates when the pathname changes. This recursive function `determineActive` takes in an option and, if it has a link, checks to see if the link is indeed active, and if not it recursively checks any submenus to see if any children's link is active.

Hopefully now the Sidenav component is working properly!

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nl1q524bjukl7o7fp9ud.png)

And as you can see, the entire tree is active,  even if everything is collapsed:

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vmezhynsdhl216dr8rjo.png)

There you have it! I hope this article was insightful and helps you find good use cases for recursion in React Components!

Signing off,

~ Sean Hurwitz