common-mistakes

Using Prop Drilling

The code provided in this article is simplified as much as possible to avoid overwhelming the reader with code and to convey the essence of the problem and its solution. In interactive examples, heading tags (`h1...h6`) are replaced with span tags to prevent potential conflicts with the main page structure.

Prop drilling — is the practice of passing a single property through multiple levels of components from parent to child. This approach is often used in React applications to pass data down the component tree. However, it can lead to difficulties in maintaining code and deteriorate performance when passing data through many levels.

Ideally, it's best to avoid passing props more than two levels deep. Let's consider the problem with an example code that has 3 layers:

Code implementation
1interface GrandChildComponentProps {
2 data: string;
3}
4
5const GrandChildComponent: FC<GrandChildComponentProps> = ({ data }) => {
6 return (
7 <Layer
8 title="GrandChild"
9 colored
10 >
11 <Text>{data}</Text>
12 </Layer>
13 );
14};
15
16interface ChildComponentProps {
17 data: string;
18}
19
20const ChildComponent: FC<ChildComponentProps> = ({ data }) => {
21 return (
22 <Layer
23 colored
24 title="Child"
25 >
26 <GrandChildComponent data={data} />
27 </Layer>
28 );
29};
30
31interface ParentComponentProps {
32 data: string;
33}
34
35const ParentComponent: FC<ParentComponentProps> = ({ data }) => {
36 return (
37 <Layer
38 title="Parent"
39 colored
40 >
41 <ChildComponent data={data} />
42 </Layer>
43 );
44};
45
46const initialData = 'Hello!';
47
48const dataToSet = 'Hello from the faraway `Container`!';
49
50const Container: FC = () => {
51 const [data, setData] = useState(initialData);
52
53 const isDataSet = data !== initialData;
54
55 const handleSetData = () => {
56 setData(dataToSet);
57 };
58
59 return (
60 <Layer
61 title="Container"
62 colored
63 >
64 <ParentComponent data={data} />
65 <div className="flex w-full justify-center">
66 <Button
67 disabled={isDataSet}
68 onClick={handleSetData}
69 >
70 {isDataSet ? 'Data Set' : 'Set Data'}
71 </Button>
72 </div>
73 </Layer>
74 );
75};
76
An interactive output of the code above

Although technically you can use as many layers as needed, it's important to remember that prop drilling can cause the following issues:

  • Performance: Prop drilling can lead to unnecessary re-renders caused by updating props at each level. In React, any component that receives new props will re-render. Intermediate components that simply pass props further down will also re-render, which can negatively impact the performance of the application in the long run.

  • Support and Scalability: The deeper the level of prop drilling, the more difficult it becomes to track how data is passed within the application. In an application with a deep component tree, when data is passed through many intermediate components, it becomes challenging to understand where the data originates from and where it's used. This complicates the understanding of the data structure and usage, making it difficult to support and extend the code. For example, if a new intermediate component needs to be added, all components above and below in the tree must be updated to maintain the new data structure. This can lead to multiple errors and increased testing time.

Approaches to Solve Prop Drilling Problem

React Context

Using the createContext method and the useContext hook allows you to create and use contexts to pass data to deeper levels of components without the need to drill props through each level. This approach can be thought of as "teleporting" data from the top of the tree to any branch below. Let's see an example of how to solve the issue using this built-in React tool.

We create a context using createContext, which is then used via the useContext hook to extract data placed in it at the top level. In Container, data is passed to DataContext.Provider through the value parameter, wrapping ParentComponent, allowing it and its child components to access this data. In this example, everything is contained in one file for simplicity and demonstration purposes. Of course, in a real project, each component should be created in a separate file.

Code implementation
1export const DataContext = createContext<string | null>(null);
2
3export const useDataContext = (): string => {
4 const context = useContext(DataContext);
5
6 if (!context) {
7 throw new Error(
8 '`useDataContext` must be used within a `DataProvider`'
9 );
10 }
11
12 return context;
13};
14
Code implementation
1const GrandChildComponent: FC = () => {
2 const data = useDataContext();
3
4 return (
5 <Layer
6 title="GrandChild"
7 colored
8 >
9 <Text>{data}</Text>
10 </Layer>
11 );
12};
13
14const ChildComponent: FC = () => {
15 return (
16 <Layer
17 colored
18 title="Child"
19 >
20 <GrandChildComponent />
21 </Layer>
22 );
23};
24
25const ParentComponent: FC = () => {
26 return (
27 <Layer
28 title="Parent"
29 colored
30 >
31 <ChildComponent />
32 </Layer>
33 );
34};
35
36const initialData = 'Hello!';
37
38const dataToSet = 'Hello from React Context!';
39
40const Container: FC = () => {
41 const [data, setData] = useState(initialData);
42
43 const isDataSet = data !== initialData;
44
45 const handleSetData = () => {
46 setData(dataToSet);
47 };
48
49 return (
50 <Layer
51 title="Container"
52 colored
53 >
54 <DataContext.Provider value={data}>
55 <ParentComponent />
56 </DataContext.Provider>
57 <div className="flex w-full justify-center">
58 <Button
59 disabled={isDataSet}
60 onClick={handleSetData}
61 >
62 {isDataSet ? 'Data Set' : 'Set Data'}
63 </Button>
64 </div>
65 </Layer>
66 );
67};
68
An interactive output of the code above

Restructuring Components

Sometimes the problem can be solved by revising the hierarchy of components and redistributing functionality to minimize the depth of data passing. For example, if you have a component that simply passes data along, you can merge its functionality with a child or parent component, thus reducing the number of levels of data passing. In the example below, we eliminate the GrandChildComponent by placing its content directly inside ChildComponent:

Code implementation
1interface ChildComponentProps {
2 data: string;
3}
4
5const ChildComponent: FC<ChildComponentProps> = ({ data }) => {
6 return (
7 <Layer
8 colored
9 title="Child"
10 >
11 <Text>{data}</Text>
12 </Layer>
13 );
14};
15
16interface ParentComponentProps {
17 data: string;
18}
19
20const ParentComponent: FC<ParentComponentProps> = ({ data }) => {
21 return (
22 <Layer
23 title="Parent"
24 colored
25 >
26 <ChildComponent data={data} />
27 </Layer>
28 );
29};
30
31const initialData = 'Hello!';
32
33const dataToSet = 'Hello from the nearby `Container`!';
34
35const Container: FC = () => {
36 const [data, setData] = useState(initialData);
37
38 const isDataSet = data !== initialData;
39
40 const handleSetData = () => {
41 setData(dataToSet);
42 };
43
44 return (
45 <Layer
46 title="Container"
47 colored
48 >
49 <ParentComponent data={data} />
50 <div className="flex w-full justify-center">
51 <Button
52 disabled={isDataSet}
53 onClick={handleSetData}
54 >
55 {isDataSet ? 'Data Set' : 'Set Data'}
56 </Button>
57 </div>
58 </Layer>
59 );
60};
61
An interactive output of the code above

Using State Managers

In more complex applications, state management libraries (state managers) like Redux, MobX, or Zustand can be used to manage state. Although such solutions require significant overhead and may be excessive for simple applications, they're useful when it's necessary to synchronize state between multiple components or when one state depends on another. A state manager helps avoid prop drilling by providing global state accessible to any component without the need to drill props. This simplifies the structure of components and improves performance. Let's see how Zustand handles this:

Code implementation
1interface IDataStore {
2 data: string;
3 isDataSet: boolean;
4 setData: (newData: string) => void;
5 resetStore: () => void;
6}
7
8const zustandInitialData = 'Hello !';
9
10const initialData = {
11 data: zustandInitialData,
12 isDataSet: false,
13};
14
15const useDataStore = create<IDataStore>((set) => ({
16 ...initialData,
17 setData: (newData) => set({ data: newData, isDataSet: true }),
18 resetStore: () => set({ ...initialData }),
19}));
20
Code implementation
1const dataToSet = 'Hello from Zustand!';
2
3const DataButton: FC = () => {
4 const setData = useDataStore((state) => state.setData);
5
6 const isDataSet = useDataStore((state) => state.isDataSet);
7
8 const handleResetStore = useDataStore((state) => state.resetStore);
9
10 const handleSetData = () => {
11 setData(dataToSet);
12 };
13
14 return (
15 <div className="flex w-full justify-center space-x-4">
16 <Button
17 disabled={isDataSet}
18 onClick={handleSetData}
19 >
20 {isDataSet ? 'Data Set' : 'Set Data'}
21 </Button>
22 <Button
23 disabled={!isDataSet}
24 onClick={handleResetStore}
25 >
26 Reset store
27 </Button>
28 </div>
29 );
30};
31
32const GrandChildComponent: FC = () => {
33 const data = useDataStore((state) => state.data);
34
35 return (
36 <Layer
37 title="GrandChild"
38 colored
39 >
40 <Text>{data}</Text>
41 </Layer>
42 );
43};
44
45const ChildComponent: FC = () => {
46 return (
47 <Layer
48 colored
49 title="Child"
50 >
51 <GrandChildComponent />
52 </Layer>
53 );
54};
55
56const ParentComponent: FC = () => {
57 return (
58 <Layer
59 title="Parent"
60 colored
61 >
62 <ChildComponent />
63 </Layer>
64 );
65};
66
67const Container: FC = () => {
68 return (
69 <Layer
70 title="Container"
71 colored
72 >
73 <ParentComponent />
74 <DataButton />
75 </Layer>
76 );
77};
78
An interactive output of the code above

Each of these approaches has its pros and cons. For example, React Context is well suited for small to medium-sized applications but can become cumbersome when scaled. On the other hand, a state manager provides powerful tools for state management but requires significant effort for setup and training. Restructuring components is not always suitable and may be impractical in some cases, as it may require significant changes to the architecture of the application and lead to code maintenance challenges. The choice of the appropriate tool depends on the specific requirements of your project and the structure of your application.

It's important to remember that using prop drilling itself is not a mistake. It's a normal part of React development that only becomes a problem with excessive use (more than two levels deep). For example, for small applications or components with simple data, prop drilling may be entirely appropriate. Always strive to keep your code clean, readable, and easy to maintain.

Last updated on by @skrykateSuggest an improvement on Github repository.