šŸ“Š State Management in React: Why Prop Drilling Becomes a Problem and How Zustand Fixes It

TAMWA KAMGA KAMGA BRANDON - Oct 9 - - Dev Community

When you're building an application with React, managing the state of your componentsā€”their data, actions, and how this information flows between componentsā€”is an ongoing challenge. At first, it seems simple with props and state, but as your application grows, the situation can quickly become messy. This problem has a name: prop drilling.

In this article, we'll first explore the difficulties caused by prop drilling in a React application, and then we'll discover how a solution like Zustand simplifies state management and makes your code easier to read and maintain. Ready? Letā€™s dive in! šŸš€


āš” Prop Drilling: When State Sharing Gets Out of Control

Imagine you're developing a user profile management application. At first, everything works smoothly. You have an App component managing the state of the user list, and you want to display this list in a child component UserList. Hereā€™s a very simplified example:

function App() {
  const [users, setUsers] = useState([
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ]);

  return <UserList users={users} />;
}

function UserList({ users }) {
  return (
    <ul>
      {users.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

It works wellā€¦ until it gets complicated.

As soon as you want to add additional features (modify, delete users, manage multiple filters), the state needs to be shared across several child components. Each new feature means passing more and more props:

function App() {
  const [users, setUsers] = useState([...]);

  const updateUser = (id, newName) => {
    setUsers(users.map(user => user.id === id ? { ...user, name: newName } : user));
  };

  const deleteUser = (id) => {
    setUsers(users.filter(user => user.id !== id));
  };

  return (
    <UserList
      users={users}
      updateUser={updateUser}
      deleteUser={deleteUser}
    />
  );
}

function UserList({ users, updateUser, deleteUser }) {
  return (
    <ul>
      {users.map((user) => (
        <UserItem
          key={user.id}
          user={user}
          updateUser={updateUser}
          deleteUser={deleteUser}
        />
      ))}
    </ul>
  );
}

function UserItem({ user, updateUser, deleteUser }) {
  const handleUpdate = () => {
    const newName = prompt("Enter a new name:");
    updateUser(user.id, newName);
  };

  return (
    <li>
      {user.name}
      <button onClick={handleUpdate}>Edit</button>
      <button onClick={() => deleteUser(user.id)}>Delete</button>
    </li>
  );
}
Enter fullscreen mode Exit fullscreen mode

Problems with Prop Drilling:

  1. Prop propagation: You constantly have to pass updateUser and deleteUser through multiple levels of components, even if some components donā€™t need those functions to function.
  2. Increased complexity: If you want to add more features, each component needs to be updated to pass new props.
  3. Reduced readability: The code becomes harder to read and maintain, especially when components start having many props.

šŸ¦ø Zustand: A Simple and Effective Solution

To avoid this chaos, solutions like Zustand allow you to create a global state that any component can access without needing to pass props down the component tree.

With Zustand, you define a global store that manages the state and actions (like updateUser and deleteUser). All components can access it directly, without having to pass props through each level of your application.

Step 1: Create a Zustand Store

First, we create a store with Zustand that manages the user list and the actions to update and delete users:

import { create } from 'zustand';

const useUserStore = create((set) => ({
  users: [
    { id: 1, name: "Alice" },
    { id: 2, name: "Bob" },
  ],
  updateUser: (id, newName) => set((state) => ({
    users: state.users.map(user => user.id === id ? { ...user, name: newName } : user)
  })),
  deleteUser: (id) => set((state) => ({
    users: state.users.filter(user => user.id !== id)
  })),
}));
Enter fullscreen mode Exit fullscreen mode

Step 2: Use the Store in Components

Now, instead of passing props throughout the component tree, each component can simply connect to the store to get the state or actions it needs:

function UserList() {
  const users = useUserStore((state) => state.users);
  return (
    <ul>
      {users.map((user) => (
        <UserItem key={user.id} user={user} />
      ))}
    </ul>
  );
}

function UserItem({ user }) {
  const updateUser = useUserStore((state) => state.updateUser);
  const deleteUser = useUserStore((state) => state.deleteUser);

  const handleUpdate = () => {
    const newName = prompt("Enter a new name:");
    updateUser(user.id, newName);
  };

  return (
    <li>
      {user.name}
      <button onClick={handleUpdate}>Edit</button>
      <button onClick={() => deleteUser(user.id)}>Delete</button>
    </li>
  );
}
Enter fullscreen mode Exit fullscreen mode

šŸ”‘ Advantages of Zustand:

  1. No more unnecessary props: Components no longer need to pass props unnecessarily. They can directly access the global state and actions through the store.
  2. Cleaner, more maintainable code: State management is centralized. The code is easier to read, understand, and maintain.
  3. Scalability: As your application grows, you can add new features to the store without needing to modify the component structure.

āš–ļø Conclusion: Why Use Zustand for State Management in React

Prop drilling can quickly make your code complex, difficult to maintain, and error-prone. With Zustand, you simplify state management by allowing direct access to the global state from any component. Whether your application is simple or complex, Zustand keeps your code clean, easy to read, and scalable.

So, are you ready to ditch prop drilling and switch to cleaner, more elegant code? šŸ˜Ž

. .
Terabox Video Player