DEV Community

Cover image for Why You Shouldn’t Pass React’s setState as a Prop: A Deep Dive
Christopher Thai
Christopher Thai

Posted on

Why You Shouldn’t Pass React’s setState as a Prop: A Deep Dive

In a React application, passing the setState function from a parent component to its children component as a prop is a common method to manage the state across different parts of the application. However, this seemingly convenient method can introduce a host of issues that can compromise the maintainability, performance, and structure of the React application. This blog post will dive into these issues, explaining why we shouldn’t pass setState as a prop in React function components and provide you with better practices and state management approaches.

Functional Components and Hooks

Before we delve into the core issues, it’s crucial to grasp the benefits of React function components enhanced by Hooks. These advancements provide superior, simpler, and more efficient ways to manage the state within the React application. Hooks enable the use of state and other React features within function components, promoting component reusability and functional patterns.

Problems with Passing setState as a Prop

Breaks the Principle of Encapsulation

Encapsulation is a crucial principle in React and component-based architecture, emphasizing that components should manage their own logic and state independently. Encapsulation ensures that components are loosely connected and can operate independently, improving testability and reusability.

View this example where the parent component passes its setState function to the child components:

// Define a functional component called ParentComponent
const ParentComponent = () => {
  // Declare a state variable called count and a function to update it called setCount, initialized to 0
  const [count, setCount] = useState(0);

  // Render the JSX elements
  return (
    <div>
      {/* Render the ChildComponent and pass the setCount function as a prop called setCount */}
      <ChildComponent setCount={setCount} />
      {/* Display the value of count */}
      <p>Count: {count}</p>
    </div>
  );
};

// Define a functional component called ChildComponent
const ChildComponent = ({ setCount }) => {
  // Render a button that triggers the setCount function when clicked and increments the count by 1
  return <button onClick={() => setCount(count => count + 1)}>Increment</button>;
};
Enter fullscreen mode Exit fullscreen mode

In this code example, the ChildComponent directly changes or manipulates the state of ParentComponent, which makes it tightly connected with the parent and less reusable in other contexts.

One metaphor about encapsulation and how passing setState as a prop breaks the principle of encapsulation is thinking encapsulation is like having your own garden where you control what grows and how it’s maintained. Passing setState as a prop is like letting your neighbor decide when to water your plants. It disrupts the independence of your garden, blending the boundaries between your space and theirs.

Increase Complexity

Passing the setState down as a prop from the parent component to the child component raises the complexity of the component hierarchy. It can change the source of the state changes and make it harder to track how and where the state is getting updated or changed. All of these can make it more challenging to diagnose and fix issues, especially in large applications with deep and many component trees.

These can also make it more complex when trying to refactor the codes. Just changing the state structure in the parent components may also require changing the child components that get and receive the setState function, which can lead to a codebase that will be hard and resistant to change.

One metaphor about this is passing a setState as a prop is like adding extra switches and levers to every room in a house that controls the same light. It complicates knowing which switch was used to turn on the light, adding unnecessary complexity to a simple action.

Performance Concerns

If the setState is misused, it can lead to performance issues and unnecessary re-renders. Suppose a child’s components randomly update the parent’s state. In that case, it can lead to a re-render of the parent and its entire subtree, regardless of whether the update logically and reasonably requires such a widespread re-render. React works hard to update the apps smoothly, but it can slow things down if state changes happen more often than needed. This is especially true for big and complex applications with large numbers of components.

One metaphor about this is passing a setState as a prop, which is like having too many cooks in the kitchen. Just as too many cooks can slow down meal preparation by getting in each other’s way, too many components trying to manage the same state can slow down the app by triggering unnecessary updates and re-renders.

Better State Management Approaches

Given the drawback of passing setState as a prop, let’s explore an alternative state management pattern that adheres to React’s best practices.

Lifting State Up

The “lifting state up” pattern involves moving the state to the nearest common ancestor of the component that needs it. This helps centralize state management and avoid the need to move or pass the state around.

// Define a functional component called ParentComponent
const ParentComponent = () => {
  // Declare a state variable called count and a function to update it called setCount, initialized to 0
  const [count, setCount] = useState(0);

  // Define a function called incrementCount that increments the count by 1
  const incrementCount = () => setCount(prevCount => prevCount + 1);

  // Render the JSX elements
  return (
    <div>
      {/* Render the ChildComponent and pass the incrementCount function as a prop called onIncrement */}
      <ChildComponent onIncrement={incrementCount} />
      {/* Display the value of count */}
      <p>Count: {count}</p>
    </div>
  );
};

// Define a functional component called ChildComponent
const ChildComponent = ({ onIncrement }) => {
  // Render a button that triggers the onIncrement function when clicked
  return <button onClick={onIncrement}>Increment</button>;
};
Enter fullscreen mode Exit fullscreen mode

The above pattern reuses the child component, ensuring a clear and orderly flow of state, which aids in maintaining structure and clarity.

One metaphor about lifting state up is like moving a water tank to the roof of an apartment building. This way, gravity ensures water flows down and distributes to all apartments evenly.

Using Context API

For more global state management needs, the Context API provides an excellent and efficient solution to share states across the entire component tree with prop drilling.

// Create a context
const CountContext = React.createContext();

function ParentComponent() {
  const [count, setCount] = useState(0); // State
  const incrementCount = () => setCount(count + 1); // Function to increment the count

  return (
    // Wrap the child component with the context provider
    <CountContext.Provider value={{ count, incrementCount }}> // Provide the value to the context 
      <ChildComponent /> // Child component
    </CountContext.Provider> // Close the context
  );
}

function ChildComponent() {
  const { incrementCount } = useContext(CountContext); // Access the value from the context

  return <button onClick={incrementCount}>Increment</button>; // Render a button that triggers the increment function
}
Enter fullscreen mode Exit fullscreen mode

The Context API facilitates an improved method for managing and accessing state throughout the components tree. This approach allows for streamlined state sharing across the application, which improves state management efficiency and component communication.

One metaphor about Context API is that it is like a public bulletin board in a building: once a message is posted, anyone in the building can see it without needing to pass notes from person to person.

Custom Hooks

Custom hooks in React are reusable functions that let you share logic and stateful behavior between components. They encapsulate both the state and related logic, making them an efficient tool for distributing functionality across components.

// Custom hook to manage the count state
const useCounter = (initialValue = 0) => {
  const [count, setCount] = useState(initialValue); // State variable to hold the count value
  const incrementCount = () => setCount(count + 1); // Function to increment the count

  return { count, incrementCount }; // Return the count value and increment function
};

// Parent component using the custom counter hook
const ParentComponents = () => {
  const { count, incrementCount } = useCounter(); // Destructure the count value and increment function from the custom hook

  return (
    <div>
      <ChildComponent onIncrement={incrementCount} /> // Render the child component and pass the increment function as a prop
      <p>Count: {count}</p> // Display the count value
    </div>
  );
};

// Child component that receives the increment function as a prop
const ChildComponents = ({ onIncrement }) => { 
  return <button onClick={onIncrement}>Increment</button>; // Render a button that triggers the onIncrement function when clicked
};
Enter fullscreen mode Exit fullscreen mode

Custom Hooks enabled modular sharing of logic across components, enhancing and improving the readability, organization, and conciseness of the application. This approval streamlines development, making maintaining an efficient and clean codebase easier.

One metaphor about custom hooks is they are like toolkits that you can carry around, allowing you to reuse tools (logic and state management) in different projects (components) effortlessly.

Conclusion

Passing ‘setState’ as a prop in React can result in some bad outcomes. It tends to violate the principle of encapsulation, where the components are supposed to manage their own logic and state independently. This can add unnecessary complexity to the application’s structure and degrade the performance through unnecessary and inefficient rendering processes. Luckily, React offers more effective state management techniques that prevent these issues. Those techniques include “lift state up” to a common parent component, utilizing the Context API for widespread state access, and designing custom hooks to shared state logic from a parent component to the child component. These techniques and strategies can surgically improve React applications’ efficiency, scalability, and maintainability.

Top comments (1)

Collapse
 
vitorgultzgoff profile image
Vitor Gultzgoff

Sometimes setting up a hook for a non-reusable module/functionality seems too much, but it is a clearer approach for sure.

Well-detailed topic, nice work!!