useState hook

1. Introduction

In React, the useState hook is a fundamental tool for managing state in functional components. It allows you to add state to your functional components without converting them into class components. useState hook enables you to declare state variables and update them within functional components.

const [state, setState] = useState(initialState)
const [count, setCount] = useState(initialCount)

Here, state and count are state variables.

Call useState at the top level of your component to declare a state variable.

import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(30);
  const [count, setCount] = useState(0);
  const [name, setName] = useState('James');
  // ...

The convention is to name state variables like [something, setSomething] using array destructuring. The initialState is the initial value of the state. You can pass any type of value in the useState. The function when passed in the useState has a special meaning. If a function is passed as an argument to the useState, will be treated as an initializer function. The initializer function should be pure, should take no arguments, and should return a value of any type. React calls your initializer function when initializing the component, and store its return value as the initial state.

React will store the next state, render your component again with the new values, and update the UI.

useState is a Hook, so you can only call it at the top level of your component or your own Hooks. It cannot be called inside loops or conditions. If you find yourself needing to use useState within a loop or condition, the recommended approach is to extract the logic into a new component and manage the state within that component. This ensures that state management remains organized and follows React’s guidelines for hook usage.

In React’s Strict Mode, the initializer function provided to useState will be invoked twice during component initialization. This is done to assist developers in identifying accidental impurities in their code. It’s important to note that this behavior is exclusive to development environments and doesn’t impact production builds. As long as your initializer function is pure (which is recommended), this duplication won’t affect the functionality of your component. One of the results from these calls will be disregarded by React.

2. Returns

The useState hook in React returns an array containing exactly two elements:

  1. The current state. This represents the current value of the state. During the first render, it will match the initialState you have passed.
  2. The set function. This function allows you to update the state to a different value. When you call this function with a new state value, React will re-render the component to reflect the updated state.

3. set functions to set nextState

The set function provided by useState allows you to modify the state to a new value, prompting a re-render of the component. set functions do not have a return value. You have two options for updating the state using this function:

  • Direct Value: You can pass the new state value directly to the set function. You can pass any type of value.
  • Functional Update: Alternatively, you can provide a function to the set function. This function calculates the next state based on the previous state. This approach is useful for cases where the new state depends on the previous state. If you pass a function as nextState, it will be treated as an updater function. Your updater function, when used in React, must adhere to certain principles:
    • Purity: It should be a pure function, meaning it doesn’t modify any external variables and produces the same output for the same input.
    • Single Argument: It should accept the pending state (the current state before the update) as its only argument.
    • Return Next State: It should return the next state, which will replace the current state.

React will put your updater function in a queue and re-render your component. During the next render, React will calculate the next state by applying all of the queued updaters to the previous state.

Your screen will not be updated if the next state is equal to the previous state, as determined by an Object.is comparison.

Calling the set function does not change the current state in the already executing code:

function handleClick() {
  console.log(name); // Still "James"!

It only affects what useState will return starting from the next render.

4. Updating state based on the previous state

Suppose the count is 42. This handler calls setCount(count + 1) three times:

function handleClick() {
  setCount(count + 1); // setCount(42 + 1)
  setCount(count + 1); // setCount(42 + 1)
  setCount(count + 1); // setCount(42 + 1)

However, after one click, count will only be 43 rather than 45. This is because calling the set function does not update the count state variable in the already running code. So each setCount(count + 1) call becomes setCount(43).

To solve this problem, you may pass an updater function to setCount instead of the next state:

function handleClick() {
  setCount(a => a + 1); // setCount(42 => 43)
  setCount(a => a + 1); // setCount(43 => 44)
  setCount(a => a + 1); // setCount(44 => 45)

Here, a => a + 1 is your updater function. It takes the pending state and calculates the next state from it.

React puts your updater functions in a queue. Then, during the next render, it will call them in the same order:

  1. a => a + 1 will receive 42 as the pending state and return 43 as the next state.
  2. a => a + 1 will receive 43 as the pending state and return 44 as the next state.
  3. a => a + 1 will receive 44 as the pending state and return 45 as the next state.

There are no other queued updates, so React will store 45 as the current state in the end.

In React, state is considered read-only, so you should replace it rather than mutate your existing objects. In case of object and array, to set new state, you should not mutate your object or array. Instead, create new object or new array representing new state.

form.firstName = 'Roger';  // incorrect way of setting state

// Correct way of setting state
  firstName: 'Roger'

5. When to use updater function?

If you find that the state you’re setting is directly related to the previous state, it’s often beneficial to write an updater function. This approach ensures that your updates are based on the current state, maintaining consistency and predictability.

However, if the new state depends on the previous state of multiple state variables or if there’s complex logic involved, you might consider combining these related state variables into a single object. By using a reducer, you can manage these state updates more effectively and maintain a clear structure in your codebase. This approach can enhance readability and simplify the management of complex state transitions.

6. Passing the initializer and passing the initial state directly

React saves the initial state once and ignores it on the next renders.

function MyComponent() {
  const [state, setState] = useState(createInitialState());
  // ...

The result of createInitialState() is only used for the initial render, but here you’re calling this function on every render. This may lead to waste when it involves generating extensive arrays or executing costly computations.

To solve this, you may pass it as an initializer function to useState instead:

function MyComponent() {
  const [state, setState] = useState(createInitialState);
  // ...

Notice the difference between createInitialState() and createInitialState.

This example passes the initializer function, so the createInitialState function only runs during initialization. It does not run when component re-renders, such as when you type into the input.

7. Resetting state with a key

The key attribute is often used when rendering lists. However, you can reset a component’s state by passing a different key to a component.

If you want to reset the entire component tree’s state, pass a different key to your component.

8. “Too many re-renders” error

Too many re-renders. React limits the number of renders to prevent an infinite loop. This means that you’re unconditionally setting state during render, so your component enters a loop: render, set state (which causes a render), render, set state (which causes a render), and so on.

9. Conclusion

In conclusion, the useState hook is a powerful tool in React that enables functional components to manage state effectively. By leveraging this hook, developers can simplify their codebase, enhance reusability, and improve performance. Through this tutorial, we’ve explored how to utilize useState to create and update state variables within functional components, enabling dynamic and interactive user interfaces. With a solid understanding of useState, developers can confidently build more efficient and maintainable React applications. As you continue your journey in React development, remember to leverage the useState hook to its fullest potential, unlocking the full capabilities of functional components in React.