如何在React中更新嵌套状态

446 阅读4分钟

在React中,我们可以使用状态设置器函数来更新状态。在函数组件中,状态设置器是由useState 钩子提供的。使用一个按钮来增加一个count ,看起来像这样。

import { useState } from 'react';

function MyComponent() {
  const [count, setCount] = useState(0);
  return (
    <button
      onClick={() => {
        setCount(count + 1);
      }}
    >
      Increment
    </button>
  );
}

同样地,在一个类组件中增加一个count ,看起来是这样的。

class MyComponent extends React.Component {
  constructor() {
    this.state = { count: 0 };
  }

  render() {
    return (
      <button
        onClick={() => {
          this.setState({ count: this.state.count + 1 });
        }}
      >
        Increment
      </button>
    );
  }
}

(注意:今后,我将只关注函数组件,但我们讨论的一切都可以很容易地转化为类组件。)

在这一点上是没有争议的!但是,如果我们有嵌套状态呢?也就是说,我们需要更新我们的状态对象中一个(或多个)层次的属性?

让我们学习一下在不同情况下如何做的一些惯例。

更新一个级别的属性

在一个新的例子中,我们有一个有状态的user ,它有一个email 属性和password 属性(想一下注册表)。我们的组件可能看起来像这样。

function SignupForm() {
  const [user, setUser] = useState({ email: '', password: '' });
  return (
    <form>
      <label htmlFor="email">Email address</label>
      <input id="email" name="email" value={user.email} />
      <label htmlFor="password">Password</label>
      <input id="password" name="password" value={user.password} />
    </form>
  );
}

很好,但是当用户键入一个输入时,我们如何更新我们的状态?你可能会想做下面这样的事情,但这是错误的!不要这样做。

不要这样做。

<input
  id="email"
  name="email"
  value={user.email}
  onChange={(e) => {
    user.email = e.target.value;
    setUser(user);
  }}
/>

我们刚刚突变了我们的user 状态对象,这在React中是一个大忌。React的差异化算法将其视为与我们之前的状态相同的对象(它同一个对象),因此可能不会为我们更新DOM。

那么我们应该怎么做呢?

浅层复制!

你在React中通常看到的模式是浅层复制一个对象。我们可以像这样使用传播操作符来做这件事。

<input
  id="email"
  name="email"
  value={user.email}
  onChange={(e) => {
    setUser({
      ...user,
      email: e.target.value,
    });
  }}
/>

我们将user 设置为内存中新创建的对象,以使React满意,使用传播操作符复制之前的user 对象,然后用我们自己的值覆盖email 属性。这可以在我们的密码字段中重复进行。

<input
  id="password"
  name="password"
  value={user.password}
  onChange={(e) => {
    setUser({
      ...user,
      password: e.target.value,
    });
  }}
/>

这感觉是多余的

如果这感觉是多余的,那是因为它是多余的!我们有一个选择,就是创建一个变化处理程序,处理user 的任何属性。 请注意,下面的例子依赖于我们输入的name 属性,以完美地映射到我们的user 状态对象。

function SignupForm() {
  const [user, setUser] = useState({ email: '', password: '' });

  function onUserChange(e) {
    setUser({
      ...user,
      [e.target.name]: e.target.value,
    });
  }

  return (
    <form>
      <label htmlFor="email">Email address</label>
      <input
        id="email"
        name="email"
        value={user.email}
        onChange={onUserChange}
      />
      <label htmlFor="password">Password</label>
      <input
        id="password"
        name="password"
        value={user.password}
        onChange={onUserChange}
      />
    </form>
  );
}

突然间,我们的代码感觉干净多了!

向下走多个层次

有时,我们的状态对象甚至更加复杂。请看下面的例子。

function SignupForm() {
  const [user, setUser] = useState({
    email: '',
    password: '',
    settings: { subscribe: false },
  });

  function onUserChange(e) {
    setUser({
      ...user,
      [e.target.name]: e.target.value,
    });
  }

  return (
    <form>
      <label htmlFor="email">Email address</label>
      <input
        id="email"
        name="email"
        value={user.email}
        onChange={onUserChange}
      />
      <label htmlFor="password">Password</label>
      <input
        id="password"
        name="password"
        value={user.password}
        onChange={onUserChange}
      />
      <label>
        <input
          type="checkbox"
          checked={user.settings.subscribe}
          onChange={(e) => {
            // What here?
          }}
        />{' '}
        Subscribe to the newsletter
      </label>
    </form>
  );
}

事实证明,我们可以浅浅地一直复制下去。让我们为我们的复选框添加一个自定义的onChange 处理器。

<label>
  <input
    type="checkbox"
    checked={user.settings.subscribe}
    onChange={(e) => {
      setUser({
        ...user,
        settings: {
          ...user.settings,
          subscribe: e.target.checked,
        },
      });
    }}
  />{' '}
  Subscribe to the newsletter
</label>

你可以看到我们是如何使用多个传播操作符来一路浅层复制我们的对象的。

用一个深层对象变化处理程序来实现花样翻新

如果我们愿意,我们可以用我们之前创建的onUserChange 函数来处理我们的状态对象的任何层次的值。

function SignupForm() {
  const [user, setUser] = useState({
    email: '',
    password: '',
    settings: { subscribe: false },
  });

  function onUserChange(e) {
    const path = e.target.name.split('.');
    const finalProp = path.pop();
    const newUser = { ...user };
    let pointer = newUser;
    path.forEach((el) => {
      pointer[el] = { ...pointer[el] };
      pointer = pointer[el];
    });
    pointer[finalProp] =
      e.target.type === 'checkbox' ? e.target.checked : e.target.value;
    setUser(newUser);
  }

  return (
    <form>
      <label htmlFor="email">Email address</label>
      <input
        id="email"
        name="email"
        value={user.email}
        onChange={onUserChange}
      />
      <label htmlFor="password">Password</label>
      <input
        id="password"
        name="password"
        value={user.password}
        onChange={onUserChange}
      />
      <label>
        <input
          type="checkbox"
          name="settings.subscribe"
          checked={user.settings.subscribe}
          onChange={onUserChange}
        />{' '}
        Subscribe to the newsletter
      </label>
    </form>
  );
}

我们假设我们的目标的name 将代表状态对象向下的路径。例如,emailpassword 处于对象的顶层,而subscribesettings (settings.subscribe) 之下。

我们的变化处理程序通过. 来分割目标的名字,然后向下潜入对象,浅浅地复制每个提供的路径。checkbox它将最后的道具设置为目标的value (如果目标的类型是checked )。

这值得吗?

这取决于你是否值得这样的抽象。如果你有一个很长的表格,一个单一的变化处理程序可能真的很好,但它也依赖于开发人员可能忘记或搞错的点符号惯例。此外,我不认为这很容易与Typescript配合。

总结

正如你所看到的,有几种不同的方法来更新React中的嵌套状态--有些相对简单,有些则比较复杂。不管你采取什么方法,只要确保你没有突变之前的状态,因为React不会用之前的对象引用重新渲染一个状态。