Understanding State Management in React: Avoiding Pitfalls with Custom Hooks
Geovany
Posted on February 17, 2024
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>
);
};
EmailVerification.tsx
export const EmailVerification = () => {
const { checkEmailVerified } = useEmailVerification();
return <button onClick={checkEmailVerified}>Refresh</button>;
};
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 };
};
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>
);
};
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>;
};
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
Posted on February 17, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
February 17, 2024