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.
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.
To manage the app state, we can create a custom hook useDetailsPreview
:
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}
/>
</>
);
};
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.
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.
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>
);
};
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.
To add, Micheal Jackson, a co-founder of Remix, shared his opinion on Twitter:
He defended his opinion by pointing out that React is fundamentally about composition and context solutions are a sort of cheating:
He also records a video about it that I recommend watching.
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.
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:
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.
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.
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.