A Case for Props Drilling in React with TypeScript

I recently worked on a story to replace a dynamic, clever use of React’s useContext hook with a technique called props drilling. For some developers, this might seem like a step backward. In our case, however, the combination of props drilling and TypeScript’s safety and editor support allowed for a simpler, more bug-proof development experience.

What Is Props Drilling?

When a higher component owns a piece of data that a lower component needs, it’s called props drilling. However, if there are other components in between the owning component and the consuming component, the developer is left taking this data in as props just to pass it down the chain.

Many React developers feel that props drilling is an anti-pattern. Intermediate components take in props that don’t relate to their own behavior, just to pass them on.

For instance, this snippet demonstrates foo and bar being “drilled” through the Next component:

import * as React from "react";

type Props = {
  foo: string;
  bar: (baz: string) => string;
};

const App: React.FunctionComponent = () => (
  <Next foo="Nick" bar={input => `How are you, ${input}`} />
);

const Next: React.FunctionComponent<Props> = props => (
  <Final foo={props.foo} bar={props.bar} />
);

const Final: React.FunctionComponent<Props> = props => (
  <p>{`${props.bar(props.foo)}`}</p>
);

The Problem

There are several solutions to the “problem” of props drilling, including higher-order components, render props, composition via children props (a slightly different flavor of render props), and finally, React’s context API. There’s a fair amount of writing on the topic, but I will describe our particular case, in which the naive approach of props drilling was actually more beneficial.

Our app has several user experiences which render different versions of the same “page” components. To understand why, think about an “administrative” versus “normal” system user viewing the same data. Users need to be able to navigate in and out of these pages. If an administrative user loads a page, and then clicks the close button, they need to navigate back to another administrative page. If a normal user views the same page and clicks a button linking to yet another page, they need to see the normal user version of that page.

To handle these contextual links, our team created a “dynamic link provider” component. We wrapped a stateful component around almost the entire app. This state contained all of the currently active dynamic links.

Then, we used a “provide dynamic links” component with a combination of useEffect and useContext hooks to mutate the high-level component’s state. Farther down the line, the buttons and actions that need the dynamic links could access the high-level state through the useContext hook.

It was a clever system, and for the most part, it worked well. However, it presented some challenges. It was hard to determine where currently active links originated. Also, a page reload erased the high-level component’s state. And the tree of rendered components might not specify the same return routes.

Our Solution

We returned to the “less advanced” technique of props drilling to solve these problems.

We added routes to the top level of the app so each link “context” would map to a single route. Then, we made each route at the top of the app responsible for providing the necessary context as props. Lower components drill this context down to the links and actions that need them.

Now, the props required by our top-level app header component force the developer to choose the navigational context of any new page or route they add. If they fail to do so, TypeScript throws compile time errors. A page reload won’t destroy that context, since navigating to the same route again will deterministically generate the same props, and therefore context, at the top level.

Conclusion

Our app still contains other uses of the context API that relate to things like visual themes, which almost every visual component needs. Drilling this information through every component would provide no benefit and add lots of noise to the code. It was clearer to drill some contextual information through a few layers of components. Most of the components we drill through now have no other use case outside of their current component tree. We split them out to keep files readable, not to be usable elsewhere.

Don’t be afraid to reach for the technique of props drilling when it makes sense. It fits well with React’s top-down data model, and it requires little digging through a complex codebase to understand where data originates.