© Bohua Xu

props daring

Jul 17, 2021 · 7min

Introduction to Props Drilling

Props drilling (sometimes called prop tunneling) occurs when you need to pass data through multiple layers of components that don’t need the data themselves but only help pass it along. This article explores different approaches to handle props drilling in React applications.

Component Separation Strategy

Before diving into solutions, it’s important to understand a fundamental principle:

Separate components that change from components that don’t change

This separation is a key performance optimization strategy that can often eliminate the need for complex solutions like useCallback or useContext.

Common Props Drilling Pattern

Basic Example with Props Drilling

Here’s a typical example of props drilling where data is passed through multiple component layers:

import React from "react";

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Component: {count}</div>
      <C1 count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

const C1 = ({ count }) => {
  console.log("C1 rendered");
  return (
    <div>
      <div>Component C1</div>
      <C2 count={count} />
    </div>
  );
};

const C2 = ({ count }) => {
  console.log("C2 rendered");
  return (
    <div>
      <div>Component C2</div>
      <C3 count={count} />
    </div>
  );
};

const C3 = ({ count }) => {
  console.log("C3 rendered");
  return (
    <div>
      <div>Component C3 - Count: {count}</div>
    </div>
  );
};

export default App;
import React from "react";

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Component: {count}</div>
      <C1 count={count} />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

const C1 = ({ count }) => {
  console.log("C1 rendered");
  return (
    <div>
      <div>Component C1</div>
      <C2 count={count} />
    </div>
  );
};

const C2 = ({ count }) => {
  console.log("C2 rendered");
  return (
    <div>
      <div>Component C2</div>
      <C3 count={count} />
    </div>
  );
};

const C3 = ({ count }) => {
  console.log("C3 rendered");
  return (
    <div>
      <div>Component C3 - Count: {count}</div>
    </div>
  );
};

export default App;

Problems with Props Drilling

  1. Maintenance Issues: When the application grows, props drilling makes code harder to maintain
  2. Component Reusability: Components become tightly coupled
  3. Performance Impact: Unnecessary re-renders of intermediate components
  4. Code Readability: Props chains become difficult to track

Solution 1: Using Context API

While Context API can solve props drilling, it’s often overused. Here’s an example:

const CountContext = React.createContext(0);

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <CountContext.Provider value={{ count }}>
      <div>Parent Component: {count}</div>
      <C1 />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </CountContext.Provider>
  );
};

// Intermediate components don't need to receive or pass count
const C1 = () => (
  <div>
    <div>Component C1</div>
    <C2 />
  </div>
);

const C2 = () => (
  <div>
    <div>Component C2</div>
    <C3 />
  </div>
);

const C3 = () => {
  const { count } = React.useContext(CountContext);
  return (
    <div>
      <div>Component C3 - Count: {count}</div>
    </div>
  );
};
const CountContext = React.createContext(0);

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <CountContext.Provider value={{ count }}>
      <div>Parent Component: {count}</div>
      <C1 />
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </CountContext.Provider>
  );
};

// Intermediate components don't need to receive or pass count
const C1 = () => (
  <div>
    <div>Component C1</div>
    <C2 />
  </div>
);

const C2 = () => (
  <div>
    <div>Component C2</div>
    <C3 />
  </div>
);

const C3 = () => {
  const { count } = React.useContext(CountContext);
  return (
    <div>
      <div>Component C3 - Count: {count}</div>
    </div>
  );
};

Context API Considerations

  1. Pros:

    • Eliminates props drilling
    • Makes state globally accessible where needed
    • Simplifies component interfaces
  2. Cons:

    • Can make component reuse more difficult
    • May cause unnecessary re-renders if not properly optimized
    • Requires default values to avoid errors
    • Can be overkill for simple state management

Solution 2: Component Composition with children

The most elegant solution often involves using React’s children prop:

const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Component: {count}</div>
      <C1>
        <C2>
          <C3 count={count} />
        </C2>
      </C1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

const C1 = ({ children }) => (
  <div>
    <div>Component C1</div>
    {children}
  </div>
);

const C2 = ({ children }) => (
  <div>
    <div>Component C2</div>
    {children}
  </div>
);

const C3 = ({ count }) => (
  <div>
    <div>Component C3 - Count: {count}</div>
  </div>
);
const App = () => {
  const [count, setCount] = React.useState(0);
  return (
    <div>
      <div>Parent Component: {count}</div>
      <C1>
        <C2>
          <C3 count={count} />
        </C2>
      </C1>
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

const C1 = ({ children }) => (
  <div>
    <div>Component C1</div>
    {children}
  </div>
);

const C2 = ({ children }) => (
  <div>
    <div>Component C2</div>
    {children}
  </div>
);

const C3 = ({ count }) => (
  <div>
    <div>Component C3 - Count: {count}</div>
  </div>
);

Benefits of Component Composition

  1. Better Performance:

    • Intermediate components only re-render when their own props change
    • No unnecessary prop passing
  2. Improved Maintainability:

    • Clear component hierarchy
    • Easier to understand data flow
    • More flexible component structure
  3. Enhanced Reusability:

    • Components are more loosely coupled
    • Easier to move and refactor components
    • More natural composition patterns

Best Practices

  1. Start Simple:

    • Begin with props drilling if the component tree is shallow
    • Only introduce Context or composition when needed
  2. Choose the Right Solution:

    • Use props drilling for simple, shallow hierarchies
    • Use Context for truly global state (theme, user data, etc.)
    • Use composition for complex component hierarchies
  3. Performance Optimization:

    • Implement React.memo() for pure components
    • Use composition to prevent unnecessary re-renders
    • Consider state management libraries for complex applications

Conclusion

Props drilling isn’t always a problem that needs to be solved. For simple applications or shallow component trees, it’s often the most straightforward solution. However, as your application grows, consider using component composition with the children prop as your first alternative, before reaching for Context or other state management solutions.

Remember: The key to managing props in React is finding the right balance between simplicity and scalability for your specific use case.

CC BY-NC-SA 4.0 2021-PRESENT © Bohua Xu