Skip Main Navigation
Ben IlegboduBen Ilegbodu

Four characters can optimize your React component

How making use of useState lazy initialization can speed up your React function component

Saturday, October 03, 2020 · 3 min read

So… yeah the title is kinda clickbait-y, but it’s kinda true. Take a look at these two code snippets.

First:

// Example 1

const Counter = () => {
  const [count, setCount] = useState(
    Number.parseInt(window.localStorage.getItem(cacheKey)),
  )

  useEffect(() => {
    window.localStorage.setItem(cacheKey, count)
  }, [cacheKey, count])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </div>
  )
}

Second:

// Example 2

const Counter = () => {
  const [count, setCount] = useState(() =>
    Number.parseInt(window.localStorage.getItem(cacheKey)),
  )

  useEffect(() => {
    window.localStorage.setItem(cacheKey, count)
  }, [cacheKey, count])

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => setCount((prevCount) => prevCount - 1)}>-</button>
      <button onClick={() => setCount((prevCount) => prevCount + 1)}>+</button>
    </div>
  )
}

Can you spot the difference? Yes? Good eye! 🔬 If not, I’ll give you a hint and zero in on the useState calls.

First:

// Example 1

const [count, setCount] = useState(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

Second:

// Example 2

const [count, setCount] = useState(() =>
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

How about now? 😄 They look almost the same right? Ok, I won't keep you in suspense (pun intended). 😂

The difference is the code for the initialization of the state. The first example retrieves the value from localStorage, parses it into an integer, and then sets it as the initial value for the count state.

// Example 1

const [count, setCount] = useState(
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

The second example is almost similar, except it passes a function that retrieves the value from localStorage, parses it into an integer, and returns the value.

// Example 2

const [count, setCount] = useState(() =>
  Number.parseInt(window.localStorage.getItem(cacheKey)),
)

Because of the terseness of arrow functions with an implicit return value, the difference between the first and second examples is only 4 characters (plus some whitespace). And depending on what we’re doing to get the initial value (in our case reading from localStorage and parsing the value), adding those 4 characters can improve the performance of your React function component.

Passing a function to useState instead of a direct value is called lazy initialization. According to the useState docs, you use lazy initialization with useState when the calculation of the initial state value is an expensive computation. This is because the lazy initialization function is only executed the first time when the state is created. On subsequent re-renders, the function is disregarded.

As a reminder, the way the useState hook works is that the first time Counter is rendered, it creates the count state with its initial value. Then when we call setCount, the entire Counter function gets called again and count has its updated value. And this re-rendering happens every time the count state is updated. All the while, that initial value is never used again.

So in the first example, if we’re reading the value from localStorage for every re-render, but we only needed its value for the first render, we’re doing a bunch of unnecessary computation. The second example using the lazy initialization function prevents that unnecessary computation.

Still a tiny bit confused? Let’s rewrite the first example to hopefully make things a bit clearer. Instead of passing the value from localStorage directly to useState, let's store it in a variable first and then pass it to useState instead.

// Example 1

const Counter = () => {
  const initialValue = Number.parseInt(window.localStorage.getItem(cacheKey))
  const [count, setCount] = useState(initialValue)

  // rest of the component
}

Now we can see that every time we re-render and call Counter again, we’re retrieving the value from localStorage every time, even though it's ultimately only needed the very first time. The second example does always pass the function to useState every re-render, but useState only calls it the very first time. That’s why it’s called lazy initialization.

And if I fully write out the lazy initialization function, it’s even clearer how different the two examples are:

// Example 2

const Counter = () => {
  const [count, setCount] = useState(function() {
    return Number.parseInt(window.localStorage.getItem(cacheKey)),
  })

  // rest of the component
}

Since the lazy initialization function is only called once, should you use it all the time? Even for something like this:

// returning a primitive value

const Counter = () => {
  const [count, setCount] = useState(() => 0)

  // rest of the component
}

Or this:

// returning a prop or existing variable

const Counter = ({ initialCount }) => {
  const [count, setCount] = useState(() => initialCount)

  // rest of the component
}

In these cases, the initial value is a simple value or an already calculated variable. Even though the function is only being called once, there still is a cost to creating the function every time. And it’s likely that the cost of creating the function would be higher than just passing the value or variable along. This would be an over-optimization.

So when should you use lazy initialization? Like almost everything, it depends. 😄 The guideline from the docs is when you’re doing an “expensive computation.” Reading from localStorage would be an expensive computation. Using .map(), .filter(), .find(), etc. on arrays would be expensive computations. A good way to think of it is that if you have to call a function to get the value, it’s likely an expensive enough computation that it warrants using lazy initialization.

I’d likely use it to initialize state to be the current date/time:

const Clock = () => {
  const [time, setTime] = useState(() => new Date())

  useEffect(() => {
    const intervalId = setInterval(() => {
      setTime(new Date())
    }, 1000)

    return () => {
      clearInterval(intervalId)
    }
  }, [tickAmount])

  return <p>The time is {time.toLocaleTimeString()}.</p>
}

That’s it! Who knew so many words could be spent over 4 lil' ol' characters. 😅 If you’ve got any questions or other thoughts, feel free to shoot me a tweet at @benmvp.

Keep learning my friends. 🤓

Subscribe to the Newsletter

Get notified about new blog posts, minishops & other goodies


Hi, I'm Ben Ilegbodu. 👋🏾

I'm a Christian, husband, and father of 3, with 15+ years of professional experience developing user interfaces for the Web. I'm a Google Developer Expert Frontend Architect at Stitch Fix, and frontend development teacher. I love helping developers level up their frontend skills.

Discuss on Twitter // Edit on GitHub