Solve props drilling - React Context is not the only option

Karol Kosek | 2022-12-29

Passing props between multiple levels of components in the React tree is considered antipattern and called props drilling. In this article, I would like to write about the props drilling problem and ways to fix that. I will explain the concept with a quick example of the User Panel app.

App description

App UI preview In this section, I will describe the matter of the app that we'll be working on. The app renders a simple User Panel view which contains <UserDetailsPreview /> and <UserDetailsForm />. The first one displays user data, while the second one gives our app the ability to change first name, surname, and username. There's an opportunity for creating reusable <UserDetailsInfo /> and <UserDetailsInput /> components.

App UI preview with components highlighted

To manage the app state, we can create a custom hook useDetailsPreview:

Component tree

Currently, the implementation of our User Panel app looks like this:

const UserPanelApp = () => { const { username, lastName, firstName, setUsername, setLastName, setFirstName } = useUserDetails(); return ( <> <Heading as="h1" textAlign="center" p="8" color="lightseagreen"> User panel </Heading> <UserDetailsDisplay username={username} firstname={firstName} lastname={lastName} /> <UserDetailsForm username={username} firstname={firstName} lastname={lastName} setUsername={setUsername} setLastName={setLastName} setFirstName={setFirstName} /> </> ); };

A challenge to solve

Because both <UserDetailsDisplay /> and <UserDetailsForm /> are using the data from useUserDetails hook, we have to keep the state at the highest level. There are multiple props passed to the <UserDetailsForm />. Keep in mind the number of properties would be bigger if our user panel was to be extended. We aim to make this solution scalable.

Component tree - props drilled app

What's more, UserDetailsForm does not use its props itself - it only passes the data to the respective reusable <UserDetailInput /> and <UserInfoPreview /> components:

const UserDetailsForm = ({ username, setUsername, lastname, setLastName, setFirstName, firstname }: UserDetailsFormProps) => { return ( <VStack alignItems="flex-start" border="2px dotted black" my="16" mx="8" p="8" > <Heading as="h2" color="lightseagreen"> User details form </Heading> <HStack> <UserDetailsInput label="Username" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} /> <UserDetailsInput label="First name" placeholder="First name" value={firstname} onChange={(e) => setFirstName(e.target.value)} /> <UserDetailsInput label="Last name" placeholder="Last name" value={lastname} onChange={(e) => setLastName(e.target.value)} /> </HStack> </VStack> ); };

We want to find a solution that will enable us to pass the needed data to the right components and avoid props drilling antipattern.

Let's use react context!

One solution that comes to mind is using React Context API. We create UserDetailsContext and, inspired by Kent C Dodds solution, we create a custom hook useUserDetailsContext;

export const useUserDetailsContext = () => { const ctx = useContext(UserDetailsContext); if (!ctx) throw new Error("You should use context inside the provider!!!"); return ctx; };

Now, we wrap our UserPanel in context provider. Because we use context, we do not need to pass all the data as props to UserDetailsDisplay and UserDetailsForm. Our UserPanel component looks very clean:

const UserPanelApp = () => { const userDetails = useUserDetails(); return ( <UserDetailsContext.Provider value={userDetails}> <Heading as="h1">User panel</Heading> <UserDetailsDisplay /> <UserDetailsForm /> </UserDetailsContext.Provider> ); };

In order to access the data, we just use useUserDetailsContext. E.g. in <UserDetailsDisplay>:

const UserDetailsDisplay = () => { const { username, firstName, lastName } = useUserDetailsContext(); return ( <VStack> <Heading as="h2">User details preview</Heading> <UserInfoPreview label="Username" value={username} /> <UserInfoPreview label="First name" value={firstName} /> <UserInfoPreview label="Last name" value={lastName} /> </VStack> ); };

No component usage without wrapping the component with a provider

As it is stated in ReactJS docs: Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult.

Our child components work fine in our app because they are wrapped in ContextProvider. It could make our test development trickier. We need to remember to wrap the component while writing tests in RTL and creating stories with Storybook.

React is fundamentally about composition

To add, Micheal Jackson, a co-founder of Remix, shared his opinion on Twitter:

mj-opinion-about-putting-everything-in-context

He defended his opinion by pointing out that React is fundamentally about composition and context solutions are a sort of cheating:

mj-opinion-about-putting-everything-in-context

He also records a video about it that I recommend watching.

Composition API

Children prop

Official react documentation encourages developers to prefer composition over inheritance and use the children prop. It allows one to choose what child component is rendering from a parent level.

const ChildComponent = ({children}) => { return <div> <h1>I'm a child component</h2> {children} </div> }; const ParentComponent = () => { return <div> <h1>I'm Malenia, Blade of Miquella...<h1> <ChildComponent> <span>...And I have never known defeat</span> </ChildComponent> </div> }

Using children's props is a part of the renderProps pattern.

The proposed solution

Now that we know about composition we can modify UserDetailsDisplay and UserDetailsForm. The components will render children props instead of <UserInfoPreview>/<UserDetailsInput>:

const UserDetailsForm = ({ children }: UserDetailsFormProps) => { return ( <VStack bg="darkred"> <Heading as="h2">User details form</Heading> <HStack>{children}</HStack> </VStack> ); };

Now, our CompositionApiUserPanel looks like this:

const UserAppPanel = () => { const { username, lastName, firstName, setUsername, setLastName, setFirstName } = useUserDetails(); return ( <> <Heading as="h1">User panel</Heading> <UserDetailsDisplay> <UserInfoPreview label="Username" value={username} /> <UserInfoPreview label="First name" value={firstName} /> <UserInfoPreview label="Last name" value={lastName} /> </UserDetailsDisplay> <UserDetailsForm> <UserDetailsInput label="Username" placeholder="Username" value={username} onChange={(e) => setUsername(e.target.value)} /> <UserDetailsInput label="First name" placeholder="First name" value={firstName} onChange={(e) => setFirstName(e.target.value)} /> <UserDetailsInput label="Last name" placeholder="Last name" value={lastName} onChange={(e) => setLastName(e.target.value)} /> </UserDetailsForm> </> ); };

Yeah, the JSX is longer than the solution with context API. Using children prop, make your code look harder to read in complex problems. However, as far as this case is concerned, I think it still looks clean. We also make our component tree less nested, as we pass reusable components in UserPanel as props:

Component Tree - composition api solution

Reuse your components more easily!

As React Context is a very clean solution to props drilling problems, in less complicated issues we can consider using Composition API instead. Not only does it make the react tree less nested, but it also makes our components more flexible. Since we define the children prop as React.ReactNode, we can find more cases to reuse our components.

Is it the best solution?

Using Composition API looks very smooth in easier cases. In more complex business logic, your wide JSX could be very hard to read. If you want to avoid that, maybe using react context will be a better solution for your problem.

Final thoughts

The beautiful (and sometimes very challenging) thing about React is that is an unopinionated library. Due to that fact, there is more than one solution or recommendation for solving an issue.

React context is a very efficient tool that you can use in your projects. However, sometimes Composition API is an easier solution to avoid props drilling. I want to point it out by writing this article.

You can read more about the topic

Code