All you need to know about React Hooks: useState

All you need to know about React Hooks: useState

This is the first post in the React Hooks series. Throughout this series, we will cover every React hook in detail and discuss its implementation. Additionally, we'll explore practical examples with mini projects, best practices, and dos and don'ts. By the end of this series, you'll have a comprehensive understanding of React hooks and how to leverage them effectively in your projects.

" I’ve found Hooks to be a very powerful abstraction — possibly a little too powerful. As the saying goes, with great power comes great responsibility."

- Bryan Manuele

Hooks are extremely abstracted and we don't know how it works under the hood. In this series of blogs, we'll break this abstraction and look deep into these hooks and how they work.

ReactJS is a popular Javascript library for developing user interfaces. React provides multiple inbuilt hooks to perform different tasks. We use Hooks to add stateful logic to our functional components.

Introduction

useState hook is used for managing states within the functional components. States are used to manage and store some data within a component. Whenever a state changes React will automatically re-render the component to display the updated value.

useState take only one parameter, the value you want your initial state to have initially. It returns an array with two values, that we get using array destructuring.

  1. Initial State: During the first re-render this will have the value that you passed as the parameter.

  2. set Function: The set Function will let you update the value of the state to a different value and will trigger a re-render if the value provided in the set function is different.

Note: useState being a hook can only be called on top or in your custom hook. You can't call it inside a function, loop or condition.

Basic Syntax

Before using Hooks in your code you need to import them from React.

Note: Every hook starts with use in the name, here's how you can import and use react hooks.

import { useState } from 'react';

How to declare useState hook

const [name, setName] = useState("Aryan");
const [count, setCount] = useState(0);
const [check, setCheck] = useState(false);

A pictorial representation of useState hook, const [name, setName] = useState("Aryan"), it has a declaration, and return an array of a initial state, an updater function and tasks the value of initial state as argument.

Example:

import { useState } from 'react';

export default function MyInput() {
  const [text, setText] = useState('hello');

  function handleChange(e) {
    setText(e.target.value);
  }

  return (
    <>
      <input value={text} onChange={handleChange} />
      <p>You typed: {text}</p>
      <button onClick={() => setText('hello')}>
        Reset
      </button>
    </>
  );
}

Set Function

The set function lets you update the current value of the state to a different value and trigger a re-render. You can pass the new value of the state directly into the function or a function that returns a new state.

Note: If you're passing a function to the set function, it must be pure, it must take the previous state as the argument and return a new state.

The set function only updates the state variable for the next render. If you read the variable in the already executing function after calling the set function, you will still get the old value that was before your call.

export default function Counter() {
    const [num, setNum] = useState(0);
    return (
        <>
            <h1>{num}</h1>
            <button onClick={() => {
                setNum(num + 1) // 0 + 1
                setNum(num + 1) // 0 + 1
                setNum(num + 1) // 0 + 1
            }}> +3 </button>
        </>
    )
}

However, in the above example, the value of "num" will only be increased by "1" after each click due to the way state updates work in React. Because state value changes only after re-render. As a result, the value of "num" remains unchanged, no matter how many times it is called. It will only change after the whole code inside the block has finished running.

Queuenum
num + 10 + 1
num + 10 + 1
num + 10 + 1

So, what happens here is -

  1. setNum(num + 1): num is 0 so setNum(0 + 1).

    • React will take this into account and will update the num on the next re-render.
  2. But before a re-render occurs another setNum(num + 1) is called.

    • So, now num does not have the updated value because a re-render hasn't occurred yet but the initial value which is 0 so setNum(0 + 1).
  3. Another call is made to setNum(num + 1) before the re-render.

    • The result will be the same as a re-render only will be triggered after the given block of code executes fully.

    • Now the third call is the end of the block and will trigger a re-render, but the num is still 0, that's why after every click we're only getting +1 and not +3.

Even though you have called the set function thrice, the num will always be 0, here we're just setting the num to 1 three times in a row.

Batching

So the question is why we have the stale data, it is simply because react uses Batching behind the scenes to update multiple state variables in only one re-render, it prevents too many re-renders to improve performance. But in the above example all the set functions in the block will be batched together, and the value of "num" will remain the same as it is not updating the previous value but overriding it.

  • React batch all the set functions together after the whole code block execution completes.

  • Then React goes through all the functions and gives the final value.

Another example of batching:-

export default function Counter() {
    const [color, setColor] = useState("red");
    return (
        <>
            <h1>{color}</h1>
            <button onClick={() => {
                setColor("blue");
                setColor("green");
                setColor("violet");
            }}>Change Color</button>
        </>
    )
}

In the above code, the final value of the color after the click will be violet.

In the picture, User and React are depicted as two different person holding a conversation, where user is telling react to update color state using updater function thrice in a row and react is picking only the last updater function

queuecolor
setColor("blue")blue
setColor("green")green
setColor("violet")violet
  • To solve this issue we can pass a pure function to setNum instead of the next state. As it'll take the pending state and calculate the next state from it.

  • The function passed inside the updater function must be pure without any side effects.

export default function Counter() {
    const [num, setNum] = useState(0);
    return (
        <>
            <h1>{num}</h1>
            <button onClick={() => {
                setNum(num => num + 1) // 0 + 1
                setNum(num => num + 1) // 1 + 1
                setNum(num => num + 1) // 2 + 1
            }}> +3 </button>
        </>
    )
}
Queuenumreturns
num => num + 10 + 11
num => num + 11 + 12
num => num + 12 + 13

Note: Clicks are not batched together, and every click is handled separately. Therefore, you do not need to worry much about it. However, you'll need the updater function if you're trying to update the state multiple time.

Best Practices

  • Always use descriptive names and follow the proper naming convention: self-describing names always make your code more readable and easy to understand.

    Handling objects or arrays in useState

  • It is crucial to avoid directly mutating the values of objects or arrays in React. The reason is that React utilizes object comparison, specifically using Object.is(), to check whether the new value passed is the same or different, When a value within an object is modified directly, React cannot distinguish the change and, as a result, fails to trigger a re-render or update the user interface.

obj.name = "aryan" // ❌ do not directly change value of a field in object.
setObj(obj) // 🚩 this will not update the state/ui.
  • To ensure proper functionality and accurate rendering in React applications, you must always replace the object or arrays with new ones in the state instead of changing their values directly.
// ✅ Here we're passing a new object with a new updated field "name"
// but everything else will be the same as we're spredding the obj (coping the whole object and replacing the value of "name")
setObj({
    ...obj,
    name: "aryan"
})

How to handle expensive initializer functions

  • When utilizing a function to provide the initial value for a state variable in React, the function must remain pure, without any side effects. Additionally, if the function is quite expensive(taking too long to return a value). React will run the same function on every re-render and will eventually replace it with the latest state.
    However, we might not need the initial function's execution after the first render, this repeated process can potentially slow down the application.
const [name, setName] = new useState(initFunction); // an expensive initial Function

To tackle this issue we can implement a technique known as "lazy initial state", This approach ensures that the function responsible for setting the initial state(e.g. initFunction) executes only once during the first render, preventing unnecessarily repeated executions in subsequent renders.

const [name, setName] = new useState(() => {
        const init = initFunction();
        return init;
    }
);

Practice Project

Below is a straightforward simple project to practice what you just learned about useState, It enables users to add their tasks to a list. To enhance the functionality, a delete option can be implemented to remove specific list items, and a "Delete All" button can be added alongside the existing "Add" button. This addition aims to provide a more comprehensive understanding of how the useState hook works in real-world scenarios.

References

  1. React.dev(preferred) - Official updated React documentation.

  2. Preserving and Resetting State - How does React update the state?

  3. Pure Functions - What is a pure function and how can you keep a function pure?

  4. Object.is - How React compares states and updates them.

  5. How objects work in State - How React handles object updates in the state.

  6. How arrays work in State - How React handles Array updates in the state.

  7. Why does my useState run twice - Why does my useState print the same value twice? It's a development-only behaviour that helps us to find mistakes and remove them subsequently. This behavior won't continue in production.

Conclusion

This article provides a comprehensive overview of React Hooks, including best practices, dos and don'ts, practical examples and a simple demo project. It explains how React handles state updates, batching, and how to handle expensive initializer functions. It also provides references to official React documentation and other resources to help readers understand how React updates the state, how to keep functions pure, and how objects and arrays work in the state.

It is one of the most basic and important hooks of all that you need to know about.

Thank you for reading the article till the end! If you found it useful, consider following me for more content. I have a whole series of blogs planned, where each React Hook will be covered in detail and much more. Don't forget to like and comment if you have any questions or feedback.

The next article will focus on the useEffect hook. Stay tuned for an in-depth exploration of this topic!


✨ Keep practicing and keep building. ✨

Did you find this article valuable?

Support Aryan Srivastava by becoming a sponsor. Any amount is appreciated!