← Go Back

useState lazy initialization

Joseph Jang
2022-11-14

Let's look at the most commonly used React API, useState.

If you've been working with React for a while, you've probably used useState. Here's a quick example of the API:
function Counter() {
  const [count, setCount] = React.useState(0)
  const increment = () => setCount(count + 1)
  return <button onClick={increment}>{count}</button>
}
function Counter() {
  const [count, setCount] = React.useState(() => 0)
  const increment = () => setCount(previousCount => previousCount + 1)
  return <button onClick={increment}>{count}</button>
}
The difference between the two codes is that useState in this example is called with a function that returns the initial state (rather than simply passing the initial state) and setCount(dispatch) is called with a function that accepts the previous state value and returns the new one.
Passing a function to useState instead of a direct value is called lazy initialization. In the official react documentation, this lazy initialization should be used when the initial value contains complex operations. The lazy initialization function is only executed when the state is first created.
This makes sense when we think about re-rendering and state updates. The initial value is only needed for the first render. However, if further re-render occurs, later on, the initial value is no longer important because we get a new state from the previous state, not from the initial state.
const [count, setCount] = useState(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)
const [count, setCount] = useState(() =>
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)
The first come block pass function call reads from localStorage as an initialValue. However, the localStorage value is only needed for the first initial state. Further execution to read localStorage value is therefore unnecessary. Therefore, if you pass a function call as an initial value to useState, you invoke a function that returns a value that is no longer needed every time the component re-render.
In the second example, unlike the previous example, initialize only occurs once which means that the function is only executed once. In subsequent renders, it is disregarded. If the initial state is the result of an expensive computation, you may provide a function instead, which will be executed only on the initial render.

So what if we treat all values as lazy initialization?

// simple primitive value
const Counter = () => {
  const [count, setCount] = useState(() => 0)

  // ...
}
// prop that is already calculated
const Counter = ({ initialCount }) => {
  const [count, setCount] = useState(() => initialCount)

  // ...
}
Each initial value is a simple value or a value that has already been calculated. Although the function is only called once due to lazy initialization, there is still the cost of creating the function. And the cost of creating a function is usually higher than the cost of creating a variable or simply passing a value. This is an example of over-optimization.

So, when to use lazy optimization?

This depends on the situation. In the document, it is said to be used when the calculation results in expensive costs. As in the previous example, accessing localStorage and manipulating arrays such as map, filter, and find can be good examples. In general, if you need to get a value through a function, and that takes an expensive computation, lazy initialization might be a good idea.
Copyright © heesungjang.dev