An in-depth guide to useState hook
Tags: react, javascript
Published: 2021-01-18 16:04:27

In this article, I will draw attention to some problems and edge cases that may occur while using the `useState` hook. This hook stores a value that is used when rendering components. It is one of the most commonly used hooks, and most of the time you can use it with no problems and it will behave as expected. But there are some exceptions, which I will cover in this article.

The topics that I will address are:
 
- [When will setState cause a re-render?](#when-will-setstate-cause-a-re-render)
- [React.memo and changing state](#reactmemo-and-changing-state)
- [setState changes are not immediately visible](#setstate-changes-are-not-immediately-visible)
- [Batching](#batching)
- [Lazy initialization](#lazy-initialization)
- [When to use setState with a callback?](#when-to-use-setstate-with-a-callback)
- [Using useState to store element reference](#using-usestate-to-store-element-reference)

## When will setState cause a re-render?

If you are familiar with class components, you might think that the hook equivalent of `this.setState` always causes a re-render. The hook method uses the [Object.is](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is) on every state change (call of setState method) and compares the previous value with the newer one. That being said, if we use the `useState` with primitive values (number, string, boolean, undefined, symbol) it will not cause a re-render if the value didn't change:

```jsx
Object.is(2, 2); // true
Object.is("value", "value"); // true
Object.is(true, true); // true
Object.is(undefined, undefined); // true
Object.is(null, null); // true
```
If we use the `useState` with `objects` or `functions`, a re-render would happen only when the reference changes:

```jsx
Object.is({}, {}); // false
Object.is([], []); // false
Object.is(() => console.log(""), () => console.log("")); // false
const foo = {a: 1};
const clone = foo;
Object.is(foo, clone); // true
Object.is(foo, {a: 1}); // false
```

This is one of the reasons why we should never directly **mutate** state because React will not detect the change and cause a re-render. It is also important when dealing with `objects/arrays` to not only set the new values but also to copy the previous ones (if you used React class components this behavior is different since React would have merged new and previous state values, so you would only need to set changes). So, if we have a complex state with nested objects:

```jsx
// complex state with nested objects
const [complexState, setComplexState] = useState({
    foo: 'bar',
    bar: 'foo',
    errors: {
         foo: 'required',
         bar: 'required'
    }
})
```
and want to change the `errors.foo` value we would do it like this:

```jsx
setComplexState({
    ...complexState,
    errors: {
         ...complexState.errors,   // we need to copy deeply nested object
        foo: 'new value'
    }
})
```

> `Object.is` used to compare states when setState is called. Primitive values are compared by value, and the complex ones by reference

## React.memo and changing state

`React.memo` will not prevent a re-render of the component where we use the `useState` hook. `React.memo` is strictly used to bailout of re-rendering **child** components when their parent re-renders. I intentionally didn't use the phrase: "when props change", since by default child components will re-render even if props stayed the same, and their parent rendered (only memoized components do a shallow comparison of props).

The mentioned behavior differentiates from its class component equivalent: `shouldComponentUpdate`, which is triggered when **both** `state` or `props` change, and can bail out of rendering even when the state changes.

> React.memo doesn't prevent re-rendering when state changes when using `useState`

## setState changes are not immediately visible

When we call `setState`, state change won’t be visible straight away. React will queue the update and sometimes even batch multiple updates so our components don't render too many times (more on that in the next [section](#batching)). 

```jsx
const [state, setState] = useState(0);

useEffect(() => {
    setState(1);
    console.log(state); // state is still 0
}, []); 

```

> State changes are visible in the next render

## Batching

It is quite common that we use multiple `useState` hooks, and call their set methods inside the same callback/useEffect call. React will by default batch those updates togheter so that our component will render only once, and not for each `setState` call:

```jsx
export default function Component() {
  const [state1, setState1] = useState(0);
  const [state2, setState2] = useState(0);

  useEffect(() => {
    console.log({ state1, state2 });
  });

  const onClick = () => {
    setState1(state1 + 1);
    setState2(state2 + 1);
  };

  return <button onClick={onClick}>Click Me</button>;
}
```
when we click on the button, in the next render, we will see updated `state1` and `state2`. There will **never** be a situation in which `state1 !== state2`.

However, there are some cases in which React will not batch updates:
- if we call `setState` methods inside an async function
- if we call `setState` inside a `setTimeout`/`setInterval`

This is usually not a big performance issue, since React renders are pretty fast, but we could end up in an **intermediate** state that we didn't expect, and it could cause our application to stop working.

If we alter the previous example, into changing the state after a timeout:

```jsx
export default function Component() {
  const [state1, setState1] = useState(0);
  const [state2, setState2] = useState(0);

  useEffect(() => {
    console.log({ state1, state2 });
  });

  const onClick = () => {
    // state is changed inside a setTimeout now
    setTimeout(() => {
      setState1(state1 + 1);
      setState2(state2 + 1);
    }, 0)
  };

  return <button onClick={onClick}>Click Me</button>;
}
```
By clicking on the set button, our component would render **twice**: the first render would update `state1`, and the second one would update `state2`.

There is an unstable API provided by React which can batch updates even inside `async/setTimeout` calls: `React.unstable_batchedupdates`. It is used internally by `React` when batching changes in event handlers or during a sync flow. 

I personally prefer to use the `useReducer` hook when dealing with interconnected states. It allows me to write exact state changes (creating a state machine of sorts) with ease and helps me eliminate the possibility of rendering our component in an intermediate state. An example of this is a simple `useFetch` hook, that clearly defines all possible states:

```jsx
function useFetch(initialState = {isLoading: true}) {
  // defined our state machine, so we are certain only these states
  // are possible and all connected states are updated in single render
  const reducer = (state, action) => {
    switch (action.type) {
        case 'request':
            return { isLoading: true };
        case 'response': {
            return { isLoading: false, data: action.data };
        }
        case 'error': {
            return { isLoading: false, error: action.error };
        }
        default:
            return state;
    }
  };
  
  const [fetchDataState, dispatch] = useReducer(reducer, initialState);

  const fetchData = async (fetchOptions, abortSignal) => {
    try {
        dispatch({ type: 'request' });
        const data = await fetcher.fetchData(fetchOptions, abortSignal);
        // this will set both loading and fetched data for next render
        dispatch({ type: 'response', data: data });
    } catch (e) {
        dispatch({ type: 'error', error: e });
    }
  };

  return { ...fetchDataState, fetchData };
}
```

> The state changes will not be batched when using inside async or setTimeout/setInterval flow

## Lazy initialization

When we want to initialize state with some potentially expensive operation, which we don't want triggering on every render (for example filtering of a big list), we can put a custom function when initializing `useState`. That function will only be called on the first render, and its results will be set as the initial value of the `useState`:

```jsx
const [state, setState] = useState(() => {
     props.initialValue.filter(...) // expensive operation
})
```

You just need to be careful that this is **only** called on the first render. If I have props, for example, that are used to initialize state, I like to prefix the prop name with `initial` or `default` to signal other devs that this value will not be synced if it changes.

> `useState` supports lazy initialization for expensive operations

## When to use setState with a callback?

`setState` has two call signatures:

- you can call it with a new value
- you can call it with a callback that receives the current value as an argument and returns the new value

The callback signature is beneficial when calling `setState` inside a `useCallback` hook so that we don't break memoization. 

If we have a simple component that uses `useState` and `useCallback` hooks with a memoized child component, and write it using the simple `setState` call signature:

```jsx
const [state, setState] = useState(0);

const onValueChanged = useCallback(() => {
     setState(state + 1);
}, [state, setState]);

return <div>
     {state}
     <MemoizedChild onValueChanged={onValueChanged }  />
</div>

```

we will ruin the optimization of our `MemoizedChild`. Since `onValueChanged ` will change on every `state` change, its reference will change as well, which will result in different props being sent to our child component (even if it doesn't use `state` in its props). This can be fixed easily by using the callback signature:

```jsx
const [state, setState] = useState(0);

const onValueChanged = useCallback(() => {
     setState(prevState => prevState + 1); // change to callback signature
}, [setState]); // remove state from dependencies since callback will provide current value

return <div>
     {state}
     <MemoizedChild onValueChanged={onValueChanged }  />
</div>

```

This will work because the `setState` reference will be **constant** throughout the whole lifecycle of our component. With this adjustment, the `MemoizedChild` component will **not** render when the state changes.

> Use the callback signature of `setState` when using with `useCallback`

## Using useState to store element reference

When you need to reference a React element you can usually use the `useRef` hook. However, what if you want to do something with the element when it is first rendered (i.e., attach an event listener, calculate dimensions, ...) or if you want to use the reference as a dependency for useEffect/useCallback? In these cases `useRef` will not trigger a re-render of our component, so we would need to combine it with the `useEffect`. You could use `useState` to get the object reference, and it would force a re-render after the element is rendered, so you could access it:

```jsx
export default function Component() {
  const [buttonRef, setButtonRef] = useState();

  useEffect(() => {
    console.log({ buttonRef });
  });

  return <button ref={setButtonRef}>Click Me</button>;
}
```

This way you would save the element reference in the state as soon as the element is rendered, and could safely use it without manually syncing it.

## Conclusion

In this article, I covered some advanced `useState` cases. Hope you enjoyed it and found it useful :)

If you are interested in learning more about this topic, you can check these links:
- [Official Documentation](https://reactjs.org/docs/hooks-state.html)
- [Dan Abramov's post about batching](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973)
- [Detailed explanation why setState doesn't update immediately](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately/58877875#58877875)