
While most paradigms don’t require React developers to know much about what is going on under the hood, working with intervals can become a bit tricky. The goal of this article is to walk you through all the ways you can work with intervals and state in react.
What is so complicated about working with intervals?
What makes intervals a bit tricky in React is that the callback function you pass to them will inevitably close (remember closures?) over the current state of your component. Your interval will see “old” state and not work the way you intended. This is often confusing for new React developers.
Let’s look at a naive implementation of a counter that increments a number every second using setInterval
:
This code does not work.
export default function App() { const [count, setCount] = useState(0); useEffect(() => { setInterval(() => { setCount(count + 1); }, 1000)
}, []); return ( <div className="App"> <h1>The current count is:</h1> <h2>{count}</h2> </div> );}
To understand why this code does not work, you have to understand how React deals with state. Dan Abramov wrote a fantastic (and very long) guide to explain this. The short version is that every render only accesses its own state. Let’s take a closer look at what happens during the first two renders of the component to understand why this code does not work:
The first time the component is rendered:
- The count variable is set to 0 (initial state)
- After the component is rendered and painted, React will execute the
useEffect
hook. TheuseEffect
hook will register the interval. The registered interval has access to the count (count is 0) variable. - After 1 second the callback will be invoked. It will call
setCount(0 + 1)
. This will NOT NOT NOT mutate the count CONSTANT. Instead, it will trigger a new render of the component with the statecount=1
(this is an entirely new variable and instance of the component).
The second time the component is rendered:
- The count variable is set to 1 (triggered by the interval)
- The component is rendered and the browser paints the UI for
count=1
- The effect does not run since the empty dependency array defines that the hook should only run on the first render
- After 1 second the callback (this code still lives in the first render) is invoked again but the callback did not magically move to the new render with a state of
count=1
. Instead, the callback only accesses thecount
value for the scope it was defined in (React is just JavaScript and has to adhere to all scoping rules). The callback will still seecount=0
and will callsetCount(0 + 1)
again. A new render will be triggered with a value ofcount=1
.
When working with React it is important to have the correct mental model. You should think of every render as a box with its own variables. If you render a component twice you get two boxes. It is absolutely possible for code to never leave the first box and run there until the end of time (this is true for our interval here).
The counter will never move to a value past count=1
. The interval callback will live in the first render and only ever see count=0
before increasing it to setCount(0 + 1)
.
Solving the state problem
After realizing this you might find yourself wondering how you should deal with these kinds of problems. Usually, this problem can be addressed in four ways. All four solutions have valid use cases. Generally, I find solution 4 to be the most useful, followed by solution 3.
Solution 1: Describe the state change
Accessing the state directly might not be as important as you think. setCount()
accepts a callback function in which you can access the current (future) state. You can simply describe how you wish to modify it.
Cons
- Works only for very simple use cases
Solution 2: Carrying a ref
Refs are a way to cheat your way out of the state encapsulation. You can use Refs to access the future state. By mutating the .current
you can get access to the updated state. Here is a working example:
Cons:
- You have to remember to update the ref on every render for this to work
- Does not follow React design patterns
Solution 3: Rebuild the effect on every render
By returning a function from useEffect
you register a cleanup function. Cleanup functions run after the effect has run. After rendering for the second time, react will cleanup the effect from the first render (and so on…).
With the help of a cleanup function, you can tear down and rebuild the interval on every render. While this sounds quite intensive, it is actually the most common way to solve this problem. Using a cleanup function means adhering to React design patterns and is generally considered good practice.
Pros:
- Follows React design patterns
Cons:
- If multiple parts of your app call
setCount
, they will each restart the interval.
Solution 4: Use the dispatch
function
Another way to cheat yourself out of the way React deals with state is to use the useReducer
hook. The default behavior of the dispatch function is to access the most current state for the component. Dispatch will let you access the “future” state.
Pros:
- Very flexible solution
- Follows React design patterns
Conclusion 🤙🏽
I hope this article will help you in gaining confidence when working with intervals and timeouts in React. Let me know if this article helped you by leaving a comment or a clap.
Plug 🔌
Are you designing REST(ish) APIs or write technical specifications? I’ve built a free api design tool. Visit api-fiddle.com to check it out!