React更新State的Object类型数据

318 阅读5分钟

State可以是Javascript的任意数据类型,包含object数据类型。但是,不能直接修改React state的object值。如果想更改一个object,需要创建一个新的object(或是复制之前的object生成新的object),然后使用新的object给state赋值。

不可变的数据

可以在State存储任意类型的JavaScript值:

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

number、string和boolean类型的数值是不可变的,或是只读的数据。替换一个值的时候触发一个重新渲染:

setX(5);

state X的值从0变到5,数值0本身是不可改变的。JavaScript不可能对基本数据类型的值(如数number、string和boolean)进行任何更改。

现在我们看下state是object类型时:

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

尽管,然而,尽管React state中的object在技术上是可变的,但是你要把它看做是不可改变的—就像number、string和boolean一样。不是修改object, 而是用新的object替换object。

把state看做仅是可读的

把state的Javascript object看作是仅仅可读的,不能修改。

下面这个例子,在state object存储的是光标位置。当在预览区域上触摸或移动光标时,红点应该会移动。但是红点一直在原始的位置:

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

1.png

问题在于这段代码。

onPointerMove={e => {
  position.x = e.clientX;
  position.y = e.clientY;
}}

这段代码直接修改了position对象。没有使用state的set函数,React不知道object改变了,所以React没有做任何回应。把state值看作是仅仅可读的,不能直接修改state的数据。 要想触发一个重新渲染,创建一个新的object并且把它传给设置state函数:

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

使用setPosition,你告诉React:

  • 用新的object替换position
  • 重新渲染这个组件

现在当在预览区域上触摸或移动光标时,红点会跟着移动。

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

使用扩展语法复制object

在前面的例子中,总是根据当前的光标位置,创建一个新的position对象。有时候需要把现有的对象的属性包含在一个对象中,作为另一个对象的一部分。例如,你想更新表单中一个字段,其他的字段都不改变。

下面的例子输入不会改变,因为onChange事件处理函数直接修改了person对象:

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    person.firstName = e.target.value;
  }

  function handleLastNameChange(e) {
    person.lastName = e.target.value;
  }

  function handleEmailChange(e) {
    person.email = e.target.value;
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

2.png

比如这一行直接修改了person对象:

person.firstName = e.target.value;

可靠的方法是创建一个新的对象并且把它传递给setPerson。在这里,只有一个字段改变了,你需要把其他的没有改变的字段复制到新的对象里:

setPerson({
  firstName: e.target.value, // New first name from the input
  lastName: person.lastName,
  email: person.email
});

你可以用…对象展开语法,这样就不需要单独复制每个属性。

setPerson({
  ...person, // Copy the old fields
  firstName: e.target.value // But override this one
});

现在表单能够正常运行了。

没有为每个输入字段声明单独的状态变量。对于大型表单,将所有数据分组在一个对象中非常方便 —— 只要正确地更新它!

import { useState } from 'react';

export default function Form() {
  const [person, setPerson] = useState({
    firstName: 'Barbara',
    lastName: 'Hepworth',
    email: 'bhepworth@sculpture.com'
  });

  function handleFirstNameChange(e) {
    setPerson({
      ...person,
      firstName: e.target.value
    });
  }

  function handleLastNameChange(e) {
    setPerson({
      ...person,
      lastName: e.target.value
    });
  }

  function handleEmailChange(e) {
    setPerson({
      ...person,
      email: e.target.value
    });
  }

  return (
    <>
      <label>
        First name:
        <input
          value={person.firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:
        <input
          value={person.lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <label>
        Email:
        <input
          value={person.email}
          onChange={handleEmailChange}
        />
      </label>
      <p>
        {person.firstName}{' '}
        {person.lastName}{' '}
        ({person.email})
      </p>
    </>
  );
}

...展开语法是“浅的 ”— 它只复制一层。这使得它更快,但这也意味着如果想要更新一个嵌套属性,不得不多次使用它。

更新一个嵌套对象

嵌套对象如下:

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

假如你想更新person.artwork.city,首选先创建一个新的artwork对象,然后创建一个新的person对象指向新的artwork对象:

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

或是写成单个函数调用:

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

实现如下:

import { useState } from 'react';

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

  function handleNameChange(e) {
    setPerson({
      ...person,
      name: e.target.value
    });
  }

  function handleTitleChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        title: e.target.value
      }
    });
  }

  function handleCityChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        city: e.target.value
      }
    });
  }

  function handleImageChange(e) {
    setPerson({
      ...person,
      artwork: {
        ...person.artwork,
        image: e.target.value
      }
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
    </>
  );
}

3.png

用Immer简化逻辑

Immer是一个比较流行的库,使你能够直接修改数据,并且负责给创建一份副本。使用Immer能够直接修改一个object:

updatePerson(draft => {
  draft.artwork.city = 'Lagos';
});

试一下Immer:

  1. 运行npm install use-immer命令将Immer添加为依赖
  2. 然后将'react'中的import {useState}替换为'use-immer'中的import {useImmer}

下面是上面的例子转换成Immer格式:

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: 'https://i.imgur.com/Sd1AgUOm.jpg',
    }
  });

  function handleNameChange(e) {
    updatePerson(draft => {
      draft.name = e.target.value;
    });
  }

  function handleTitleChange(e) {
    updatePerson(draft => {
      draft.artwork.title = e.target.value;
    });
  }

  function handleCityChange(e) {
    updatePerson(draft => {
      draft.artwork.city = e.target.value;
    });
  }

  function handleImageChange(e) {
    updatePerson(draft => {
      draft.artwork.image = e.target.value;
    });
  }

  return (
    <>
      <label>
        Name:
        <input
          value={person.name}
          onChange={handleNameChange}
        />
      </label>
      <label>
        Title:
        <input
          value={person.artwork.title}
          onChange={handleTitleChange}
        />
      </label>
      <label>
        City:
        <input
          value={person.artwork.city}
          onChange={handleCityChange}
        />
      </label>
      <label>
        Image:
        <input
          value={person.artwork.image}
          onChange={handleImageChange}
        />
      </label>
      <p>
        <i>{person.artwork.title}</i>
        {' by '}
        {person.name}
        <br />
        (located in {person.artwork.city})
      </p>
    </>
  );
}

事件处理程序变的简洁了。可以在单个组件中随意混合使用useState和useImmer。Immer是一个让事件处理函数变的简洁的好的方法,特别是在state中有嵌套复制对象会有重复的代码的时候。

参考文献: Updating Objects in State