Skip to content

React Design Patterns: Recursive Components

Updated: at 10:18 PM

Table of contents

Open Table of contents

React Design Patterns: Recursive Components

What are design patterns?

While you regularly encounter unique issues as a software engineer, you can also agree that some common issues spring up in your day-to-day code. These issues mostly have to do with software design and structure.

Design patterns can be defined as general, repeatable, and reusable solutions to commonly occurring issues in software design. Think of design patterns as tested, proven and trusted templates that guide you in writing efficient and cleaner code.

Utilizing design patterns introduces a lot of benefits to your codebase as a whole. Here are some examples of such benefits:

etcetera

Recursion In Programming

You have probably heard or seen the term “recursion” in your software engineering career; maybe you have an idea of what it means, or maybe not at all. If you have no idea what it means, don’t worry; it will be explained briefly in this section.

According to Wikipedia , recursion is a method of solving a computational programming problem where the solution depends on solutions to smaller instances of the same problem. The application of recursion usually involves calling a function within itself until a base case (or base condition) is met.

Usually, a recursive pattern involves two main components:

Base Case (or Base Condition)

The base case is the condition under which the recursive function stops calling itself. This component is extremely important in a recursive process because it prevents the function from calling itself infinitely, which could lead to a stack overflow error.

Think of a base case as the endpoint of a recursive call.

Characteristics of a Base Case:

Recursive Case

The recursive case is the part of the recursive function that breaks down the problem into smaller instances. It involves calling the function itself repeatedly using modified conditions, gradually moving towards the base case.

Characteristics of a Recursive Case:

That probably didn’t make much sense to you but do not worry. I personally believe the best way to explain and understand recursion is by using real-life analogies. So here is one I’ve prepared:

Real-Life Scenario

Imagine you are at the top of a staircase and want to get to the bottom. You could approach this task recursively by considering each step as a smaller version of the overall task.

If you get to the last step, you simply step down and you’re done, this represents the base case where the problem or task has been reduced to its simplest form.

If you’re not yet at the last step, you would have to take one step down and consider how to descend the remaining stairs till you reach the last step. This represents the recursive case.

So the overall process will be something like this:

  1. Begin at the top of the staircase
  2. If there is only one step left, step down to the bottom (base case)
  3. If there are multiple steps left:

The process continues until you reach the bottom of the staircase. Each time you take a step down, the number of steps needed to reach the bottom reduces, in other words, the problem or task becomes smaller and smaller until it reaches the base case.

The code will look something like this:

function descendStairs(stepsLeft) {
  // Base case: no more steps left
  if (stepsLeft === 0) {
    console.log("You have reached the bottom!");
    return;
  }

  // Recursive case: take one step down
  console.log(`Stepping down... ${stepsLeft} steps left to go!`);
  descendStairs(stepsLeft - 1);
}

// Start at the top with a given number of steps
descendStairs(10);

Recursive Component Pattern in React

A simple definition of recursion in React context would be:

calling a function or component repeatedly within itself until a base condition (base case) is met

This is a very powerful pattern for dealing with data structures that possess nested children. Recursive components are unique in the sense that they can be called within themselves while they traverse through nested data until the base case is reached.

Mechanism

This design pattern consists of a base case that serves as a stop for the recursive loop, as well as a recursive case that calls the function in smaller versions of itself.

Let’s take a look at a React component that is meant to display a mega menu containing several sub-menus that display when clicked.

Breaking it down, we first define our array of data with the necessary properties according to the desired configuration.

For menus with children, the subMenu property is defined with the necessary properties. The same goes for the sub-menus of sub-menus. For the menus without children, the subMenu property is left blank as an empty array.

const megaMenu = [
  {
    id: 1,
    isPregnant: false,
    title: "Home",
    url: "/home",
    subMenu: [],
  },
  {
    id: 2,
    isPregnant: true,
    title: "Products",
    url: "/products",
    subMenu: [
      {
        id: 3,
        isPregnant: true,
        title: "Electronics",
        url: "/products/electronics",
        subMenu: [
          {
            id: 4,
            isPregnant: false,
            title: "Mobile Phones",
            url: "/products/electronics/mobile-phones",
            subMenu: [],
          },
          {
            id: 5,
            isPregnant: false,
            title: "Laptops",
            url: "/products/electronics/laptops",
            subMenu: [],
          },
        ],
      },
      {
        id: 6,
        isPregnant: true,
        title: "Clothing",
        url: "/products/clothing",
        subMenu: [
          {
            id: 7,
            isPregnant: false,
            title: "Men's Clothing",
            url: "/products/clothing/mens",
            subMenu: [],
          },
          {
            id: 8,
            isPregnant: false,
            title: "Women's Clothing",
            url: "/products/clothing/womens",
            subMenu: [],
          },
        ],
      },
    ],
  },
  {
    id: 9,
    isPregnant: false,
    title: "About Us",
    url: "/about-us",
    subMenu: [],
  },
  {
    id: 10,
    isPregnant: false,
    title: "Contact",
    url: "/contact",
    subMenu: [],
  },
];

Afterwards, we will define the recursive component for this mega menu. Here there are a few things it needs to have:

The next thing is to map through our earlier defined array of data and utilize the properties within it. The Boolean isPregnant property tells whether a menu has a sub-menu or not.

Within the code, both the base case and recursive case are applied through the isPregnant property conditions. If true, the function calls itself with each subMenu data until it reaches a point where the isPregnant property is false.

const RecursiveComponent = ({ data = [] }) => {
  const [showNested, setShowNested] = useState(false);

  // handle show/hide
  const toggleNested = title => {
    setShowNested({ ...showNested, [title]: !showNested[title] });
  };

  return (
    <div className="pl-5">
      {data.map(parent => {
        return (
          <div key={parent.id}>
            <div>
              {parent.isPregnant && (
                <button
                  className="text-white"
                  onClick={() => toggleNested(parent.title)}
                >
                  {parent.title}
                </button>
              )}

              <span>{parent.title}</span>
            </div>

            <div style={{ display: !showNested[parent.title] && "none" }}>
              {parent.isPregnant && (
                <RecursiveComponent data={parent.subMenu} />
              )}
            </div>
          </div>
        );
      })}
    </div>
  );
};

Lastly, we utilize our recursive component on the UI by passing the megaMenu array of data we defined earlier:

const HomePage = () => {
  return <RecursiveComponent data={megaMenu} />;
};

export default HomePage;

It is important to note that each recursive call is accompanied by the creation of a new stack frame to store the data of each call until the base case is met. This may not be a problem for small datasets, but for larger ones, it could lead to resource-intensive memory consumption. Therefore, use recursive solutions with caution.

Use Cases

A good reason to apply the recursion design pattern would be when you have a reasonable amount of data that contains several levels of nesting, or is hierarchical in nature. Some examples include:

and so on.

Benefits

The major benefit of the recursive component approach is the simplicity it provides. An alternative to recursive design patterns would be iterative solutions but as opposed to recursive patterns, iterative approaches are usually faster but more cumbersome to write.

Additionally, recursive patterns are easier to scale because they can handle data at any arbitrary depth.

Here are some benefits worth considering:

My rule of thumb would be to use recursive design patterns only if other solutions would provide more unnecessary complexity.

Cons

Recursive design patterns could come with some concerns such as:

Conclusion

Recursive React components come with several benefits, especially in code organization, scalability and flexibility with relational data.

Take advantage of them when it is appropriate, usually after considering the performance impact on the application.

Thank you for reading till the end, (I hope you did 😉), I write articles and make content for people who want to level up to senior engineers. Follow me on my social platforms, and feel free to check out my other articles on my blog.

Happy building!