DEV Community

Geovany
Geovany

Posted on • Edited on

Understanding State Management in React: Avoiding Pitfalls with Custom Hooks

Hi Devs πŸ‘‹Β (@mgeovany on GitHub ) I'm immersed in the world of fintech startups, working on an exciting mobile app project using React Native. Along the way, I've realized the importance of documenting my development journey. Sharing insights and solutions not only helps junior developers but also deepens my understanding and advances my career.

Feel free to drop any comments or suggestions below. πŸš€

Introduction

As junior developers, one of the common stumbling blocks we encounter in React development is managing state effectively, particularly when it comes to sharing state between components. In this blog post, we'll delve into a common issue faced by many developers and explore a solution that not only fixes the problem but also provides valuable insights into proper state management in React.

Identifying the Problem:

Imagine you're working on a React application where you have a main component rendering different parts of the UI based on certain conditions. You also have a custom hook responsible for managing some state outside of the main component. Additionally, you have another component, let's call it EmailVerification, which is conditionally rendered within the main component. This EmailVerification component contains a button that should update the state managed by the custom hook. However, despite updating the state, the UI does not reflect the changes as expected.
So we will have something like this:

App.tsx

export const App = () => {
  const { emailVerified, checkEmailVerified } = useEmailVerification();

  return (
    <div>
      {emailVerified && <div>Start contract</div>}
      {!emailVerified && (
        <EmailVerification />
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

EmailVerification.tsx

export const EmailVerification = () => {

  const { checkEmailVerified } = useEmailVerification();

  return <button onClick={checkEmailVerified}>Refresh</button>;
};

Enter fullscreen mode Exit fullscreen mode

useEmailVerification.ts

export const useEmailVerification = () => {
  const [emailVerified, setEmailVerified] = useState<boolean>(false);

  const checkEmailVerified = () => {

    // here we would have the logic to handle the email verification
    const userEmailVerified = true;

    setEmailVerified(userEmailVerified);
  };

  return { emailVerified, checkEmailVerified };
};

Enter fullscreen mode Exit fullscreen mode

Understanding the Issue:

The root cause of this problem lies in the fact that multiple instances of the custom hook are being used independently in different components. Each instance maintains its own state, which leads to inconsistencies when updating the state in one component but expecting changes to reflect in another.

When you create a custom hook, you're essentially encapsulating stateful logic within it. When this hook is called within a component, React creates a separate instance of the state associated with that hook for each component instance that uses it. This means that each component instance that uses the custom hook will have its own isolated state, independent of other component instances.

This behavior is crucial for ensuring that each component maintains its own internal state and does not interfere with the state of other components. It's a fundamental principle of React's component-based architecture, where components are designed to be self-contained and modular.

The Solution:

To address this issue, we need to ensure that both components share the same state. One approach is to lift the state up to a common parent component and pass down the state and its update function as props to the child components. By doing so, we establish a single source of truth for the state, ensuring consistency across the application.

Implementing the Solution:

In our case, we refactored the code to remove the instances of the custom hook from the EmailVerification component and instead passed down the state and its update function from the main component. This allowed both components to share the same state, ensuring that updates in one component are reflected in the other.

So the new version of the code will look like this:

App.tsx

export const App = () => {

  // Custom hook called on the parent component and passed as props to EmailVerification
  const { emailVerified, checkEmailVerified } = useEmailVerification();

  return (
    <div>
      {emailVerified && <div>Start contract</div>}
      {!emailVerified && (
        <EmailVerification
          emailVerified={emailVerified}
          checkEmailVerified={checkEmailVerified}
        />
      )}
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

EmailVerification.tsx

export const EmailVerification = ({ emailVerified, checkEmailVerified }) => {
  // No new instances of the custom hook
  const handleContinueButton = async () => {
    checkEmailVerified();
    if (emailVerified) {
      console.log('email verified', emailVerified);
    }
  };

  return <button onClick={handleContinueButton}>Refresh</button>;
};
Enter fullscreen mode Exit fullscreen mode

Conlusion:

Initially, I assumed that understanding how each instance of a custom hook maintains its own state was pretty straightforward in React. But as I delved deeper, I discovered that even seasoned developers might overlook this behavior, leading to misconceptions about shared state.

It's fascinating to see that React's documentation dedicates a special section to this topic, emphasizing its importance.

References:

React Documentation: https://react.dev/learn/reusing-logic-with-custom-hooks#custom-hooks-let-you-share-stateful-logic-not-state-itself

Top comments (0)