React Hooks – useState lazy loading, synced updates and Refs Overview

by | May 8, 2021 | 0 comments

react hooks in action

The following article will go over:

  • Lazy load initial state values.
  • Making sure your state is always correctly updated.
  • useRef overview.

Lazy load initial state values

While I was working with Hooks for a while now, a big part of my current workplace’s code uses Class-based React components. Therefore I was not aware that you can improve your functional component’s performance by lazy loading initial values in the useState Hook. 

Lets look at the following example, which has an expensive computation as part of the initial state value for the data variable.

Here is the example’s code:

import { useEffect, useState } from "react";
import { Blog } from "./Blog";
import "./styles.css";

const expensiveComputation = () => {
  // Some intensive calculation
  console.log("Expensive Computation");
  return { example: "Data" };
};

const Initial = () => {
  // ---- LOOK HERE---------------------------------------
  const [data, setData] = useState(expensiveComputation());
  // ----------------------------------------------------
  const [title, setTitle] = useState("Example");
  useEffect(() => {
    // Triggering a rerender to demonstrate
    // the expensive computation on each render.
    setTimeout(() => {
      setTitle("Example title - Rerendered");
    }, 2000);
  }, []);

  return <div>{title}</div>;
};

export default function App() {
  return (
    <div className="App">
      <h1>Lazy Load Initial State</h1>
      <Initial />
      <Blog />
    </div>
  );
}

As you are about to see, the above code runs the expensive computation on each render of the component, which will take its toll on our overall component’s performance.

For this example, it will run twice, as you can see below.
* For any issues with the below example hit the Refresh Icon.

useState allows for initial lazy loading by passing a function as an initial value instead of the value itself directly.

By doing so we let React manage our initial state and call that function only once.

Let’s fix our initial state call:

// Before:  
const [data, setData] = useState(expensiveComputation());

// After:
// Passing a reference to the function expensiveComputation
const [data, setData] = useState(expensiveComputation);

// Another option is an inline function
const [data, setData] = useState(() => expensiveComputation());

This tiny, yet significant improvement has surely become very useful for me, and as such I’m sure it will become useful for you.

Side note: The arrow function only runs once, but it will be recreated each time, therefore you should prefer using the function reference option to avoid wasteful generation of the passed arrow function.

 Improved example:* For any issues with the below example hit the Refresh Icon.

Make sure your state is always correctly updated

One of my colleagues was tasked with extracting a certain component out to a new shared library we will depend on for multiple projects. As a part of this task, he needed to clear all dependencies between the current project and the component he was extracting, so it can work independently for each project.

One of the decisions we had to make was to inject common components such as a loading spinner in order to make sure the design still has a native look for each project.

When he needed to update the state of the component with a new injected component the following code looked like this:

For this example I did not add new components to an array as described above, but rather I’ve used numbers to make the code easier to understand.

import { useEffect, useState } from "react";
import { Blog } from "./Blog";
import "./styles.css";

export default function App() {
  const [state, setState] = useState({
    values: []
  });

  const addValue = (value) => setState({ values: [...state.values, value] });

  useEffect(() => {
    // Expectation: [1,2,3], Result: [3]
    for (let i = 1; i <= 3; i++) {
      addValue(i);
    }
  }, []);

  state.values.length && console.log(state.values);

  return (
    <div className="App">
      <h1>React Hooks - Synced State Update - Before</h1>
      <Blog />
    </div>
  );
}

After running the above code, the state was never updated with all the components needed, and only updated with the last value passed through the loop.
* For any issues with the below example hit the Refresh Icon.

While this was logical, we were calling an async function multiple times with the same initial value, we had the wrong expectation that the state will always update against its latest version.

To solve this at that time, I’ve suggested updating the state with an array of values instead of updating it with each value one by one.

It was a good solution at that time, but it was still bothering me that there was something I was missing about how to update our state.

After reading “React Hooks In Action”, it was clear to me how it could easily be fixed.

when updating a value by using the useState update function we destructured, we can pass a function instead of a value directly.

By doing so React will inject that function with the latest updated state making sure everything is synced.

Now we can update our state the same as before, taking into account the latest available state injected by React.

This will lead us to our desired result of a correctly updated state. 

import { useEffect, useState } from "react";
import { Blog } from "./Blog";
import "./styles.css";

export default function App() {
  const [state, setState] = useState({
    values: []
  });

  // React's injected "latestState" which
  // will always be synced with the latest state updates.
  const addValue = (value) =>
    setState((latestState) => ({ values: [...latestState.values, value] }));

  useEffect(() => {
    // Expectation: [1,2,3], Result: [1,2,3]
    for (let i = 1; i <= 3; i++) {
      addValue(i);
    }
  }, []);

  state.values.length && console.log(state.values);

  return (
    <div className="App">
      <h1>React Hooks - Synced State Update - After</h1>
      <Blog />
    </div>
  );
}

* For any issues with the below example hit the Refresh Icon.

useREf Overview

I feel the need to go over Refs because I’ve seen how it can be confusing for new developers using Hooks.

useRef:

  • It returns a mutable object that will persist for the full lifetime of the component.
  • The mutable object returned has a property named current, which holds our reference value.
  • You can supply an initial value when calling the useRef hook.
  • It can hold any mutable value we want, which is an improvement over the use of the ref attribute we used before React’s hooks, which only helps us in getting the DOM element reference
const ref = useRef(initialValue);

// ref = { current: initialValue }

Now for the important part, one of the powerful ways you can use a Reference value is to update the state of a value in your functional component without triggering a rerender.

Let’s look at the following example of two counters, one working with useState and another working with useRef.
* For any issues with the below example hit the Refresh Icon.

As you would expect, the counter using useState will always show the updated counter number. 

The useRef based counter, will still update its value but it will not trigger a rerender as mentioned above, and that’s why we won’t see the updated number until we trigger a rerender for the component.

Now when you click the above example force update button, you will see that the values have been updating for every click on the useRef counter.

*      *     *

I hope you’ve enjoyed this article, I’m sure that you are now better equipped at solving performance and state updates issues regarding the useState hook, and that now you truly understand the useRef hook.