DEV Community

Cover image for Decoding The React useState hook
vedanth bora
vedanth bora

Posted on

Decoding The React useState hook

What is useState ?

useState is a React Hook that lets you add a state variable to your component.

const [state, setState] = useState(initialState)
Enter fullscreen mode Exit fullscreen mode

Adding state to a component

Call useStateat the top level of your component to declare one or more state variables.

import { useState } from 'react';

function MyComponent() {
  const [age, setAge] = useState(21);
  const [name, setName] = useState('vedanth');

// ...
Enter fullscreen mode Exit fullscreen mode

The convention is to name state variables like [something, setSomething] using array destructuring.

useState returns an array with exactly two items:

  1. The current state of this state variable, initially set to the initial state you provided.
  2. The set function that lets you change it to any other value in response to interaction.

To update what’s on the screen, call the set function with some next state:

function handleClick() {
  setName('Batman');
}
Enter fullscreen mode Exit fullscreen mode

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


What are state variables ?

Components often need to change what’s on the screen as a result of an interaction. Like typing into the form should update the input field, clicking “next” on an image carousel should change which image is displayed, clicking “buy” should put a product in the shopping cart and so on.

Components need to “remember” things: the current input value, the current image, the shopping cart and so on. In React, this kind of component-specific memory is called state.

So what is the difference between a regular variable and a state variable ?

In React

  1. Local variables don’t persist between renders. When React renders this component a second time, it renders it from scratch—it doesn’t consider any changes to the local variables.
  2. Changes to local variables won’t trigger renders. React doesn’t realize it needs to render the component again with the new data.

So if we try to change something on the UI by changing a regular variable, react will not trigger a render and so nothing will change on the screen.

To update a component with new data, two things need to happen:

  1. Retain the data between renders.
  2. Trigger React to render the component with new data (re-rendering).

The [useState](https://beta.reactjs.org/apis/usestate) Hook provides those two things:

  1. A state variable to retain the data between renders.
  2. A state setter function to update the variable and trigger React to render the component again.

Lets try to understand this with an example to understand better.

This is counter and we are trying to update the count with a regular value

import React from "react"

export default function App() {
  let count = 0;

  function handleClick() { 
    count = count + 1;
  }

  return (
    <>
      <h1> {count} <h1>
      <button onClick={handleClick}>
        increase count
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

In the example above, React will not trigger a re-render an so nothing will change on the UI.

To solve this we need to use a state variable,

import React, { useState }  from "react"

export default function App() {
  const [count, setCount] = useState(0);

  function handleClick() { 
    setCount(count + 1);
  }

  return (
    <>
      <h1> {count} <h1>
      <button onClick={handleClick}>
        increase count
      </button>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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


💡 Calling the setfunction does not change the current state in the already executing code:

 function handleClick() { 
    setCount(count + 1);
    console.log(count) // still 0
  }
Enter fullscreen mode Exit fullscreen mode

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


A few examples using useState

  1. Counter

In this example, the count state variable holds a number. Clicking the button increments it.

https://stackblitz.com/edit/react-ts-fxpjaa?embed=1&file=App.tsx

  1. Text field

In this example, the textstate variable holds a string. When you type, handleChange
reads the latest input value from the browser input DOM element, and calls setText
to update the state.

https://stackblitz.com/edit/react-ts-tpwd62?embed=1&file=App.tsx

  1. Checkbox

In this example, the liked state variable holds a boolean. When you click the input, setLiked
updates the liked state variable with whether the browser checkbox input is checked. The liked variable is used to render the text below the checkbox.

https://stackblitz.com/edit/react-ts-7fw6wv?embed=1&file=App.tsx


How to u*pdate state based on the previous state*

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

function handleClick() {
  setCount(count + 1); // setCount(10 + 1)
  setCount(count + 1); // setCount(10 + 1)
  setCount(count + 1); // setCount(10 + 1)
}
Enter fullscreen mode Exit fullscreen mode

However, after one click, count will only be 11 rather than 13! 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(11).

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

function handleClick() {
  setCount(c => c + 1); // setCount(10 => 11)
  setCount(c => c + 1); // setCount(11 => 12)
  setCount(c => c + 1); // setCount(12 => 13)
}
Enter fullscreen mode Exit fullscreen mode

Here, c => c + 1 is your updater function. A function that calculates the next state based on the previous one in the queue.

It is a way to tell React to “do something with the state value” instead of just replacing it.

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

  1. c => c + 1 will receive 10 as the pending state and return 11 as the next state.
  2. c => c + 1 will receive 11 as the pending state and return 12 as the next state.
  3. c => c + 1 will receive 12 as the pending state and return 13 as the next state.

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

By convention, it’s common to name the pending state argument for the first letter of the state variable name, like c for count. However, you may also call it like prevCount or something else that you find clearer.

What about this event handler? What do you think number will be in the next render?

const [number, setNumber] = useState(0);

<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>

// Yes you're right , number will be 6
Enter fullscreen mode Exit fullscreen mode

💡 React may call your updaters twice in development to verify that they are pure.


How to update objects and arrays in state

You can put objects and arrays into state. In React, state is considered read-only, so you should replace it rather than mutate your existing objects. For example, if you have a form object in state, don’t update it like this:

// 🚩 Don't mutate an object in state like this:
form.firstName = 'Vedanth';
Enter fullscreen mode Exit fullscreen mode

Instead, replace the whole object by creating a new one:

// ✅ Replace state with a new object
setForm({
  ...form,
  firstName: 'Vedanth'
});
Enter fullscreen mode Exit fullscreen mode

What’s a mutation?

You can store any kind of JavaScript value in state.

const [x, setX] = useState(0);
Enter fullscreen mode Exit fullscreen mode

If you’ve working with numbers, strings, and booleans. These kinds of JavaScript values are “immutable,” meaning unchangeable or “read-only.” You can trigger a re-render to replace a value:

setX(5);
Enter fullscreen mode Exit fullscreen mode

The x state changed from 0 to 5, but the number 0 itself did not change. It’s not possible to make any changes to the built-in primitive values like numbers, strings, and booleans in JavaScript.

Now consider an object in state:

const [position, setPosition] = useState({ x: 0, y: 0 });
Enter fullscreen mode Exit fullscreen mode

Technically, it is possible to change the contents of the object itself. This is called a mutation:

position.x = 5;
Enter fullscreen mode Exit fullscreen mode

However, although objects in React state are technically mutable, you should treat them as if they were immutable—like numbers, booleans, and strings. Instead of mutating them, you should always replace them.

In other words, you should treat any JavaScript object that you put into state as read-only.

Lets try to understand this with some examples

This example holds an object in state to represent the current pointer position. The red dot is supposed to move when you touch or move the cursor over the preview area.

https://stackblitz.com/edit/react-ts-tmrc2q?embed=1&file=App.tsx

Using a single event handler for multiple fields

https://stackblitz.com/edit/react-ts-crzvrd?embed=1&file=App.tsx

Consider a nested object structure like this:

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: 'https://i.imgur.com/Sd1AgUOm.jpg',
  }
});
Enter fullscreen mode Exit fullscreen mode

If you wanted to update person.artwork.city, it’s clear how to do it with mutation:

person.artwork.city = 'New Delhi';
Enter fullscreen mode Exit fullscreen mode

But in React, you treat state as immutable! In order to change city,

setPerson({
  ...person, // Copy other fields
  artwork: { // but replace the artwork
    ...person.artwork, // with the same one
    city: 'New Delhi' // but in New Delhi!
  }
});
Enter fullscreen mode Exit fullscreen mode

How to u*pdate arrays without mutation*

In JavaScript, arrays are just another kind of object. Like with objects, you should treat arrays in React state as read-only. This means that you shouldn’t reassign items inside an array like arr[0] = 'bird', and you also shouldn’t use methods that mutate the array, such as push()
and pop().

Instead, every time you want to update an array, you’ll want to pass a new array to your state setting function. To do that, you can create a new array from the original array in your state by calling its non-mutating methods like filter()and map(). Then you can set your state to the resulting new array.

Lets understand with a few examples.

  1. Adding to an array

push() will mutate an array, which you don’t want:

setArtists( // Replace the state
  [ // with a new array
    ...artists, // that contains all the old items
    { id: nextId++, name: name } // and one new item at the end
  ]
);
Enter fullscreen mode Exit fullscreen mode
setArtists([
  { id: nextId++, name: name },
  ...artists // Put old items at the end
]);
Enter fullscreen mode Exit fullscreen mode
  1. Removing from an array

The easiest way to remove an item from an array is to filter it out. In other words, you will produce a new array that will not contain that item. To do this, use the filter method, for example:

setArtists(
  artists.filter(a => a.id !== artist.id)
);
Enter fullscreen mode Exit fullscreen mode

I hope this blog helped you understand useState hook better.

Discussion (0)