I Thought I Knew useEffect, But I Was Wrong: useEffect Misconceptions

The SaaS Enthusiast
4 min readMay 22, 2024

--

The Problem: Automatic Saving of Answers

In a recent project, I had a stepper that showed different questions. When a user provided an answer and moved to the next question, I expected the system to save the answer automatically. Based on this business requirement, I assumed that using useEffect would easily solve this problem. However, when I attempted to save the answer, the value was null. Let’s review why this happened.

Read more about some of the problems you may face, on the next article Managing Local and Cloud Data in React: A Guide to Avoiding Race Conditions

The Initial Approach: Using useEffect for Cleanup

Typically, I rely on the unmount method for these scenarios. When a component is mounted, the user interacts with it, and then the component is unmounted, making way for a new component to be mounted. This seemed like the perfect opportunity to save the answer.

const [answer, setAnswer] = useState(0);

useEffect(() => {
return () => {
saveAnswer(answer);
};
}, []);

<Button onClick={() => setAnswer(1000)}>
Set 1000
</Button>

My understanding was that saveAnswer would be executed when the component was unmounted, giving me access to the latest state of answer automatically. However, when the component was unmounted, the function executed, but answer always had the value it received when the component was mounted (0).

The First Attempt: Adding Dependencies to useEffect

Next, I tried adding the dependency in useEffect to update the value of answer so that when the component was unmounted, the function would be called with the latest value.

useEffect(() => {
return () => {
saveAnswer(answer);
};
}, [answer]);

This approach didn’t work because the function was executed every time answer changed its value, which was not the behavior I was looking for. My initial understanding was flawed: the return function is not run only when the component is unmounted if a dependency is declared on useEffect.

The function returned by the useEffect hook is indeed run when the component is unmounted. However, it’s also run before every new render if the dependencies of the useEffect have changed. In this case, with answer as a dependency, the cleanup function runs before every render caused by a change in value, as well as when the component is unmounted. To have the cleanup function run only when the component is unmounted, an empty dependency array [] should be used. But with this, the value inside the cleanup function will be the initial value captured at the time of the first render, as the useEffect closure only has access to the value state at the time it was created.

The Solution: Using useRef to Access the Latest Value

What’s useRef

useRef is a React hook that returns a mutable ref object whose .current property is initialized to the passed argument (initialValue). The returned object persists for the full lifetime of the component.

Implementing useRef

const answerRef = useRef(answer);

useEffect(() => {
answerRef.current = answer; // Keep the ref updated with the latest value
}, [answer]);

useEffect(() => {
return () => {
console.log('value', answerRef.current); // This will log the latest value
saveAnswer(answerRef.current);
};
}, []);

Why This Code Works

This solution works because useRef allows us to persist a mutable object (the ref) across re-renders. By updating answerRef.current inside a useEffect with answer as a dependency, we ensure that answerRef.current always holds the latest value of answer. When the component is unmounted, the cleanup function in the second useEffect reads the current value of answerRef.current, which is the latest value of answer, and calls saveAnswer with it. This approach avoids the pitfalls of stale closures and ensures that the latest state is always available during the unmount phase.

Conclusion

Understanding how useEffect and closures work in React is crucial for effective state management. Using useRef ensures we always access the latest state value, even during component unmount. This method avoids common pitfalls associated with stale closures and dependency arrays. Ultimately, it makes our React applications more reliable and predictable. Read more about some of the problems you may face, on the next article Managing Local and Cloud Data in React: A Guide to Avoiding Race Conditions

Empower Your Tech Journey:

Explore a wealth of knowledge designed to elevate your tech projects and understanding. From safeguarding your applications to mastering serverless architecture, discover articles that resonate with your ambition.

New Projects or Consultancy

For new project collaborations or bespoke consultancy services, reach out directly and let’s transform your ideas into reality. Ready to take your project to the next level?

Frontend + Backend

Mastering Serverless Series

Advanced Serverless Techniques

--

--