SoFunction
Updated on 2025-04-11

React 18 How to update an object in state

Reference article

Update objects in state

JavaScript values ​​of any type, including objects, can be saved in state. However, objects stored in React state should not be directly modified. Instead, when you want to update an object, you need to create a new object (or copy it) and then update state to this object.

What is mutation?

You can store any type of JavaScript value in the state.

const [x, setX] = useState(0);

Store numbers, strings, and boolean values ​​in state, which are immutable in JavaScript, meaning they cannot be changed or read-only. A re-render can be triggered by replacing their values.

setX(5);

state x from0 Become5 , but the number0 It has not changed itself. In JavaScript, no changes to built-in raw values ​​such as numbers, strings, and boolean values ​​cannot be made.

Now consider the situation where objects are stored in state:

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

Technically speaking, the object's own content can be changed.When doing this, a mutation is created

 = 5;

However, although strictly speaking, objects stored in React state are mutable, they should be treated as immutable just like they do with numbers, booleans, strings. Therefore, their values ​​should be replaced instead of modifying them.

Treat state as read-only

In other words, it shouldTreat all JavaScript objects stored in state as read-only

In the following example, an object stored in state is used to represent the current position of the pointer. When the cursor is touched or moved in the preview area, the red dot should have moved. But actually the red dot remains in its original place:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
         = ;
         = ;
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${}px, ${}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

The problem lies in the following code.

onPointerMove={e => {
   = ;
   = ;
}}

This code was directly modified in the last rendering and assigned toposition object. However, because the setting function of state is not used, React does not know that the object has been changed. So React didn't respond. Although it may be effective to modify the state directly in some cases, it is not recommended. The state that can be accessed during rendering should be regarded as read-only.

In this case, in order to really trigger a re-render,The setting function that needs to create a new object and pass it to the state

onPointerMove={e => {
  setPosition({
    x: ,
    y: 
  });
}}

By usingsetPosition , telling React:

  • Use this new object to replaceposition Value of
  • Then render the component again

You can now see that when you touch or move the cursor in the preview area, the red dot moves with the pointer:

import { useState } from 'react';
export default function MovingDot() {
  const [position, setPosition] = useState({
    x: 0,
    y: 0
  });
  return (
    <div
      onPointerMove={e => {
        setPosition({
          x: ,
          y: 
        });
      }}
      style={{
        position: 'relative',
        width: '100vw',
        height: '100vh',
      }}>
      <div style={{
        position: 'absolute',
        backgroundColor: 'red',
        borderRadius: '50%',
        transform: `translate(${}px, ${}px)`,
        left: -10,
        top: -10,
        width: 20,
        height: 20,
      }} />
    </div>
  );
}

Copy objects using Expand Syntax

In the previous example, a new one would always be created based on the current pointer's positionposition Object. But usually, I would like toexistingData is part of the new object created. For example, you might want to update only one field in the form, and the other fields still use the previous value.

In the following code, the input box will not run normally becauseonChange Modified the state directly:

import { useState } from 'react';
export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@'
  });
  function handleFirstNameChange(e) {
     = ;
  }
  function handleLastNameChange(e) {
     = ;
  }
  function handleEmailChange(e) {
     = ;
  }
  return (
    <>
      <label>
        First name:
        <input
          value={}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {}{' '}
        {}{' '}
        ({})
      </p>
    </>
  );
}

For example, the following line of code changes the state in the last rendering:

 = ;

To achieve the requirement, the most reliable way is to create a new object and pass it tosetPerson . But here, there is still a needCopy the current data into a new object, because only one of the fields has been changed:

setPerson({
  firstName: , // Get a new first name from input  lastName: ,
  email: 
});

Available... Object Expand Syntax so that each property does not need to be copied individually.

setPerson({
  ...person, // Copy all fields from the previous person  firstName:  // But override the firstName field});

The form is now running normally!

As you can see, there is no separate state for each input box. For large forms, it is very convenient to store all the data in the same object - provided that it can be updated correctly!

import { useState } from 'react';
export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@'
  });
  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: 
    });
  }
  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: 
    });
  }
  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: 
    });
  }
  return (
    <>
      <label>
        First name:
        <input
          value={}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {}{' '}
        {}{' '}
        ({})
      </p>
    </>
  );
}

Please note... The nature of the expansion grammar is "shallow copy" - it only copies one layer. This makes it execute very quickly, but also means that when you want to update a nested property, you must use the expansion syntax multiple times.

Update a nested object

Consider nested objects of the following structure:

const [person, setPerson] = useState({
  name: 'Niki de Saint Phalle',
  artwork: {
    title: 'Blue Nana',
    city: 'Hamburg',
    image: '/',
  }
});

If you want to update The value of , the method implemented by mutation is very easy to understand:

 = 'New Delhi';

But in React, state needs to be considered immutable! For modificationcity The value of the first thing to do is to create a new valueartwork Object (where the previous one is pre-filledartwork data in the object), and then create a new oneperson object and makeartwork Properties point to newly createdartwork Object:

const nextArtwork = { ..., city: 'New Delhi' };
const nextPerson = { ...person, artwork: nextArtwork };
setPerson(nextPerson);

Or, write it as a function call:

setPerson({
  ...person, // Copy data from other fields  artwork: { // Replace artwork field    ..., // Copy the data in the previous    city: 'New Delhi' // But replace the value of city with New Delhi!  }
});

Although this may seem a bit lengthy, it can effectively solve the problem for many situations:

import { useState } from 'react';
export default function Form() {
  const [person, setPerson] = useState({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: '/',
    }
  });
  function handleNameChange(e) {
    setPerson({
      ...person,
      name: 
    });
  }
  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...,
        title: 
      }
    });
  }
  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...,
        city: 
      }
    });
  }
  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...,
        image: 
      }
    });
  }
  return (
    <>
      <label>
        Name:
        <input
          value={}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{}</i>
        {' by '}
        {}
        <br />
        (located in {})
      </p>
      <img 
        src={} 
        alt={}
      />
    </>
  );
}

Use Immer to write concise update logic

If state has multiple layers of nesting, perhaps it should be considered flattened. However, if you do not want to change the data structure of state, you can use Immer to achieve the effect of nested expansion. Immer is a very popular library that allows you to write code using simple but directly modified syntax and will help you handle the copying process. By using Immer, the code written looks like "breaking the rules" and directly modifying the object:

updatePerson(draft => {
   = 'Lagos';
});

But unlike the general mutation, it does not overwrite the previous state!

Try using Immer:

  • runnpm install use-immer Add Immer dependencies
  • useimport { useImmer } from 'use-immer' Replaceimport { useState } from 'react'

Let's use Immer to implement the above example:

import { useImmer } from 'use-immer';
export default function Form() {
  const [person, updatePerson] = useImmer({
    name: 'Niki de Saint Phalle',
    artwork: {
      title: 'Blue Nana',
      city: 'Hamburg',
      image: '/',
    }
  });
  function handleNameChange(e) {
    updatePerson(draft => {
       = ;
    });
  }
  function handleTitleChange(e) {
    updatePerson(draft => {
       = ;
    });
  }
  function handleCityChange(e) {
    updatePerson(draft => {
       = ;
    });
  }
  function handleImageChange(e) {
    updatePerson(draft => {
       = ;
    });
  }
  return (
    <>
      <label>
        Name:
        <input
          value={}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{}</i>
        {' by '}
        {}
        <br />
        (located in {})
      </p>
      <img 
        src={} 
        alt={}
      />
    </>
  );
}

{
  "dependencies": {
    "immer": "1.7.3",
    "react": "latest",
    "react-dom": "latest",
    "react-scripts": "latest",
    "use-immer": "0.5.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  },
  "devDependencies": {}
}

As you can see, the event handling function has become more concise. Can be used at the same time in one component at willuseStateanduseImmer. If you want to write a more concise update processing function, Immer is a good choice, especially when there are nesting in the state and copying objects will bring duplicate code.

summary

  • Treat all states in React as non-directly modifyable.
  • When storing an object in state, modifying the object directly does not trigger re-rendering, and changes the value of state in the previous rendering "snapshot".
  • Instead of modifying an object directly, create a new version for it and trigger re-rendering by setting state to this new version.
  • You can use such {...obj, something: 'newValue'} object expansion syntax to create a copy of the object.
  • The object's expansion syntax is shallow: it has only one layer of copy depth.
  • To update nested objects, you need to create a new copy for each layer from the bottom up from the updated location.
  • To reduce duplicate copy code, use Immer.

This is the end of this article about the objects in React 18 updated. For more related React updates to state objects, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!