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 replace
position
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:
- run
npm install use-immer
Add Immer dependencies - use
import { 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 willuseState
anduseImmer
. 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!