选择状态结构

良好地构建状态可以区分一个易于修改和调试的组件,以及一个经常产生错误的组件。以下是构建状态时应考虑的一些提示。

¥Structuring state well can make a difference between a component that is pleasant to modify and debug, and one that is a constant source of bugs. Here are some tips you should consider when structuring state.

你将学习到

  • 何时使用单个与多个状态变量

    ¥When to use a single vs multiple state variables

  • 组织状态时要避免什么

    ¥What to avoid when organizing state

  • 如何解决状态结构的常见问题

    ¥How to fix common issues with the state structure

结构化状态的原则

¥Principles for structuring state

当你编写一个保存某些状态的组件时,你必须选择使用多少个状态变量以及它们的数据应该是什么形状。虽然即使使用次优状态结构也可以编写正确的程序,但有一些原则可以指导你做出更好的选择:

¥When you write a component that holds some state, you’ll have to make choices about how many state variables to use and what the shape of their data should be. While it’s possible to write correct programs even with a suboptimal state structure, there are a few principles that can guide you to make better choices:

  1. 分组相关状态。如果你总是同时更新两个或多个状态变量,请考虑将它们合并为一个状态变量。

    ¥Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable.

  2. 避免状态上的矛盾。当状态的结构使得多个状态可能相互矛盾并且 “有分歧” 时,你就会为错误留下空间。尽量避免这种情况。

    ¥Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.

  3. 避免冗余状态。如果你可以在渲染期间从组件的属性或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。

    ¥Avoid redundant state. If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.

  4. 避免状态重复。当相同的数据在多个状态变量之间或嵌套对象中重复时,很难使它们保持同步。尽可能减少重复。

    ¥Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can.

  5. 避免深度嵌套状态。层次很深的状态更新起来不是很方便。如果可能,更优先以扁平的方式构建状态。

    ¥Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.

这些原则背后的目标是使状态易于更新而不会引入错误。从状态中删除冗余和重复数据有助于确保其所有部分保持同步。这类似于数据库工程师可能希望 “规范化” 数据库结构 减少出现错误的机会。套用爱因斯坦的话,“使你的状态尽可能简单 - 但不能更简单。“

¥The goal behind these principles is to make state easy to update without introducing mistakes. Removing redundant and duplicate data from state helps ensure that all its pieces stay in sync. This is similar to how a database engineer might want to “normalize” the database structure to reduce the chance of bugs. To paraphrase Albert Einstein, “Make your state as simple as it can be—but no simpler.”

现在让我们看看这些原则是如何应用到行动中的。

¥Now let’s see how these principles apply in action.

¥Group related state

有时你可能不确定是使用单个还是多个状态变量。

¥You might sometimes be unsure between using a single or multiple state variables.

你应该这样做吗?

¥Should you do this?

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

或者这样?

¥Or this?

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

从技术上讲,你可以使用这些方法中的任何一种。但是,如果某些两个状态变量总是一起变化,那么将它们统一为单个状态变量可能是一个好主意。然后你就不会忘记始终保持它们同步,就像在这个例子中移动光标更新红点的两个坐标:

¥Technically, you can use either of these approaches. But if some two state variables always change together, it might be a good idea to unify them into a single state variable. Then you won’t forget to always keep them in sync, like in this example where moving the cursor updates both coordinates of the red dot:

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>
  )
}

将数据分组到对象或数组中的另一种情况是,当你不知道需要多少个状态时。例如,当你有一个用户可以添加自定义字段的表单时,它会很有帮助。

¥Another case where you’ll group data into an object or an array is when you don’t know how many pieces of state you’ll need. For example, it’s helpful when you have a form where the user can add custom fields.

易犯错误

如果你的状态变量是一个对象,请记住 你不能只更新其中的一个字段 而不是显式复制其他字段。例如,你不能在上面的示例中执行 setPosition({ x: 100 }),因为它根本没有 y 属性!而是,如果你想单独设置 x,你可以执行 setPosition({ ...position, x: 100 }),或者将它们分成两个状态变量并执行 setX(100)

¥If your state variable is an object, remember that you can’t update only one field in it without explicitly copying the other fields. For example, you can’t do setPosition({ x: 100 }) in the above example because it would not have the y property at all! Instead, if you wanted to set x alone, you would either do setPosition({ ...position, x: 100 }), or split them into two state variables and do setX(100).

避免状态矛盾

¥Avoid contradictions in state

这是带有 isSendingisSent 状态变量的酒店反馈表:

¥Here is a hotel feedback form with isSending and isSent state variables:

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [isSending, setIsSending] = useState(false);
  const [isSent, setIsSent] = useState(false);

  async function handleSubmit(e) {
    e.preventDefault();
    setIsSending(true);
    await sendMessage(text);
    setIsSending(false);
    setIsSent(true);
  }

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

虽然此代码有效,但它为 “不可能的” 状态敞开了大门。例如,如果忘记将 setIsSentsetIsSending 一起调用,则可能会出现 isSendingisSent 同时为 true 的情况。你的组件越复杂,就越难理解发生了什么。

¥While this code works, it leaves the door open for “impossible” states. For example, if you forget to call setIsSent and setIsSending together, you may end up in a situation where both isSending and isSent are true at the same time. The more complex your component is, the harder it is to understand what happened.

由于 isSendingisSent 绝不能同时为 true,因此最好将它们替换为一个可能采用三种有效状态之一的 status 状态变量:'typing'(初始)、'sending''sent'

¥Since isSending and isSent should never be true at the same time, it is better to replace them with one status state variable that may take one of three valid states: 'typing' (initial), 'sending', and 'sent':

import { useState } from 'react';

export default function FeedbackForm() {
  const [text, setText] = useState('');
  const [status, setStatus] = useState('typing');

  async function handleSubmit(e) {
    e.preventDefault();
    setStatus('sending');
    await sendMessage(text);
    setStatus('sent');
  }

  const isSending = status === 'sending';
  const isSent = status === 'sent';

  if (isSent) {
    return <h1>Thanks for feedback!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>How was your stay at The Prancing Pony?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        Send
      </button>
      {isSending && <p>Sending...</p>}
    </form>
  );
}

// Pretend to send a message.
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

你仍然可以声明一些常量以提高可读性:

¥You can still declare some constants for readability:

const isSending = status === 'sending';
const isSent = status === 'sent';

但它们不是状态变量,因此你无需担心它们彼此不同步。

¥But they’re not state variables, so you don’t need to worry about them getting out of sync with each other.

避免冗余状态

¥Avoid redundant state

如果你可以在渲染期间从组件的属性或其现有状态变量中计算出一些信息,则不应将该信息放入该组件的状态中。

¥If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state.

例如,拿这个表单来说。它有效,但你能在其中找到任何冗余状态吗?

¥For example, take this form. It works, but can you find any redundant state in it?

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
    setFullName(e.target.value + ' ' + lastName);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
    setFullName(firstName + ' ' + e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

此表单具有三个状态变量:firstNamelastNamefullName。但是,fullName 是冗余的。你始终可以在渲染期间从 firstNamelastName 计算 fullName,因此将其从状态中移除。

¥This form has three state variables: firstName, lastName, and fullName. However, fullName is redundant. You can always calculate fullName from firstName and lastName during render, so remove it from state.

你可以这样做:

¥This is how you can do it:

import { useState } from 'react';

export default function Form() {
  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');

  const fullName = firstName + ' ' + lastName;

  function handleFirstNameChange(e) {
    setFirstName(e.target.value);
  }

  function handleLastNameChange(e) {
    setLastName(e.target.value);
  }

  return (
    <>
      <h2>Let’s check you in</h2>
      <label>
        First name:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        Last name:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        Your ticket will be issued to: <b>{fullName}</b>
      </p>
    </>
  );
}

这里,fullName 不是状态变量。而是,它是在渲染期间计算的:

¥Here, fullName is not a state variable. Instead, it’s calculated during render:

const fullName = firstName + ' ' + lastName;

因此,更改处理程序不需要做任何特殊的事情来更新它。当你调用 setFirstNamesetLastName 时,你会触发重新渲染,然后下一个 fullName 将根据新数据进行计算。

¥As a result, the change handlers don’t need to do anything special to update it. When you call setFirstName or setLastName, you trigger a re-render, and then the next fullName will be calculated from the fresh data.

深入研究

不要在状态中镜像属性

¥Don’t mirror props in state

冗余状态的一个常见示例是这样的代码:

¥A common example of redundant state is code like this:

function Message({ messageColor }) {
const [color, setColor] = useState(messageColor);

这里,color 状态变量被初始化为 messageColor 属性。问题是,如果父组件稍后传递不同的 messageColor 值(例如,'red' 而不是 'blue'),则 color 状态变量将不会更新!状态仅在第一次渲染期间初始化。

¥Here, a color state variable is initialized to the messageColor prop. The problem is that if the parent component passes a different value of messageColor later (for example, 'red' instead of 'blue'), the color state variable would not be updated! The state is only initialized during the first render.

这就是为什么 “镜像” 状态变量中的某些属性会导致混淆。而是,直接在你的代码中使用 messageColor 属性。如果你想给它一个更短的名字,使用一个常量:

¥This is why “mirroring” some prop in a state variable can lead to confusion. Instead, use the messageColor prop directly in your code. If you want to give it a shorter name, use a constant:

function Message({ messageColor }) {
const color = messageColor;

这样它就不会与从父组件传递的属性不同步。

¥This way it won’t get out of sync with the prop passed from the parent component.

只有当你想忽略特定属性的所有更新时,“镜像” 属性进入状态才有意义。按照惯例,属性名称以 initialdefault 开头,以阐明其新值将被忽略:

¥“Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial or default to clarify that its new values are ignored:

function Message({ initialColor }) {
// The `color` state variable holds the *first* value of `initialColor`.
// Further changes to the `initialColor` prop are ignored.
const [color, setColor] = useState(initialColor);

避免状态重复

¥Avoid duplication in state

此菜单列表组件可让你从多种旅行小吃中选择一种:

¥This menu list component lets you choose a single travel snack out of several:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map(item => (
          <li key={item.id}>
            {item.title}
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

目前,它将所选条目存储为 selectedItem 状态变量中的对象。但是,这不是很好:selectedItem 的内容与 items 列表中的一项相同。这意味着有关条目本身的信息在两个地方重复。

¥Currently, it stores the selected item as an object in the selectedItem state variable. However, this is not great: the contents of the selectedItem is the same object as one of the items inside the items list. This means that the information about the item itself is duplicated in two places.

为什么这是个问题?让我们让每个条目都可编辑:

¥Why is this a problem? Let’s make each item editable:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedItem, setSelectedItem] = useState(
    items[0]
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2> 
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedItem(item);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

请注意,如果你首先在某个项目上单击 “选择”,然后对其进行编辑,输入会更新,但底部的标签不会反映编辑内容。这是因为你有重复的状态,并且你忘记更新 selectedItem

¥Notice how if you first click “Choose” on an item and then edit it, the input updates but the label at the bottom does not reflect the edits. This is because you have duplicated state, and you forgot to update selectedItem.

尽管你也可以更新 selectedItem,但更简单的解决方法是删除重复项。在此示例中,你将 selectedId 保持在状态中,而不是 selectedItem 对象(它与 items 中的对象创建重复),然后通过在 items 数组中搜索具有该 ID 的项目来获取 selectedItem

¥Although you could update selectedItem too, an easier fix is to remove duplication. In this example, instead of a selectedItem object (which creates a duplication with objects inside items), you hold the selectedId in state, and then get the selectedItem by searching the items array for an item with that ID:

import { useState } from 'react';

const initialItems = [
  { title: 'pretzels', id: 0 },
  { title: 'crispy seaweed', id: 1 },
  { title: 'granola bar', id: 2 },
];

export default function Menu() {
  const [items, setItems] = useState(initialItems);
  const [selectedId, setSelectedId] = useState(0);

  const selectedItem = items.find(item =>
    item.id === selectedId
  );

  function handleItemChange(id, e) {
    setItems(items.map(item => {
      if (item.id === id) {
        return {
          ...item,
          title: e.target.value,
        };
      } else {
        return item;
      }
    }));
  }

  return (
    <>
      <h2>What's your travel snack?</h2>
      <ul>
        {items.map((item, index) => (
          <li key={item.id}>
            <input
              value={item.title}
              onChange={e => {
                handleItemChange(item.id, e)
              }}
            />
            {' '}
            <button onClick={() => {
              setSelectedId(item.id);
            }}>Choose</button>
          </li>
        ))}
      </ul>
      <p>You picked {selectedItem.title}.</p>
    </>
  );
}

状态曾经像这样被复制:

¥The state used to be duplicated like this:

  • items = [{ id: 0, title: 'pretzels'}, ...]

  • selectedItem = {id: 0, title: 'pretzels'}

但是改完之后是这样的:

¥But after the change it’s like this:

  • items = [{ id: 0, title: 'pretzels'}, ...]

  • selectedId = 0

重复没有了,你只保留本质状态!

¥The duplication is gone, and you only keep the essential state!

现在,如果你编辑所选条目,下面的消息将立即更新。这是因为 setItems 触发了重新渲染,而 items.find(...) 会找到具有更新标题的条目。你不需要将所选条目保持在状态中,因为只有所选 ID 是必需的。其余的可以在渲染期间计算。

¥Now if you edit the selected item, the message below will update immediately. This is because setItems triggers a re-render, and items.find(...) would find the item with the updated title. You didn’t need to hold the selected item in state, because only the selected ID is essential. The rest could be calculated during render.

避免深度嵌套状态

¥Avoid deeply nested state

想象一个由行星、大陆和国家组成的旅行计划。你可能会想使用嵌套对象和数组来构建其状态,如本例所示:

¥Imagine a travel plan consisting of planets, continents, and countries. You might be tempted to structure its state using nested objects and arrays, like in this example:

export const initialTravelPlan = {
  id: 0,
  title: '(Root)',
  childPlaces: [{
    id: 1,
    title: 'Earth',
    childPlaces: [{
      id: 2,
      title: 'Africa',
      childPlaces: [{
        id: 3,
        title: 'Botswana',
        childPlaces: []
      }, {
        id: 4,
        title: 'Egypt',
        childPlaces: []
      }, {
        id: 5,
        title: 'Kenya',
        childPlaces: []
      }, {
        id: 6,
        title: 'Madagascar',
        childPlaces: []
      }, {
        id: 7,
        title: 'Morocco',
        childPlaces: []
      }, {
        id: 8,
        title: 'Nigeria',
        childPlaces: []
      }, {
        id: 9,
        title: 'South Africa',
        childPlaces: []
      }]
    }, {
      id: 10,
      title: 'Americas',
      childPlaces: [{
        id: 11,
        title: 'Argentina',
        childPlaces: []
      }, {
        id: 12,
        title: 'Brazil',
        childPlaces: []
      }, {
        id: 13,
        title: 'Barbados',
        childPlaces: []
      }, {
        id: 14,
        title: 'Canada',
        childPlaces: []
      }, {
        id: 15,
        title: 'Jamaica',
        childPlaces: []
      }, {
        id: 16,
        title: 'Mexico',
        childPlaces: []
      }, {
        id: 17,
        title: 'Trinidad and Tobago',
        childPlaces: []
      }, {
        id: 18,
        title: 'Venezuela',
        childPlaces: []
      }]
    }, {
      id: 19,
      title: 'Asia',
      childPlaces: [{
        id: 20,
        title: 'China',
        childPlaces: []
      }, {
        id: 21,
        title: 'India',
        childPlaces: []
      }, {
        id: 22,
        title: 'Singapore',
        childPlaces: []
      }, {
        id: 23,
        title: 'South Korea',
        childPlaces: []
      }, {
        id: 24,
        title: 'Thailand',
        childPlaces: []
      }, {
        id: 25,
        title: 'Vietnam',
        childPlaces: []
      }]
    }, {
      id: 26,
      title: 'Europe',
      childPlaces: [{
        id: 27,
        title: 'Croatia',
        childPlaces: [],
      }, {
        id: 28,
        title: 'France',
        childPlaces: [],
      }, {
        id: 29,
        title: 'Germany',
        childPlaces: [],
      }, {
        id: 30,
        title: 'Italy',
        childPlaces: [],
      }, {
        id: 31,
        title: 'Portugal',
        childPlaces: [],
      }, {
        id: 32,
        title: 'Spain',
        childPlaces: [],
      }, {
        id: 33,
        title: 'Turkey',
        childPlaces: [],
      }]
    }, {
      id: 34,
      title: 'Oceania',
      childPlaces: [{
        id: 35,
        title: 'Australia',
        childPlaces: [],
      }, {
        id: 36,
        title: 'Bora Bora (French Polynesia)',
        childPlaces: [],
      }, {
        id: 37,
        title: 'Easter Island (Chile)',
        childPlaces: [],
      }, {
        id: 38,
        title: 'Fiji',
        childPlaces: [],
      }, {
        id: 39,
        title: 'Hawaii (the USA)',
        childPlaces: [],
      }, {
        id: 40,
        title: 'New Zealand',
        childPlaces: [],
      }, {
        id: 41,
        title: 'Vanuatu',
        childPlaces: [],
      }]
    }]
  }, {
    id: 42,
    title: 'Moon',
    childPlaces: [{
      id: 43,
      title: 'Rheita',
      childPlaces: []
    }, {
      id: 44,
      title: 'Piccolomini',
      childPlaces: []
    }, {
      id: 45,
      title: 'Tycho',
      childPlaces: []
    }]
  }, {
    id: 46,
    title: 'Mars',
    childPlaces: [{
      id: 47,
      title: 'Corn Town',
      childPlaces: []
    }, {
      id: 48,
      title: 'Green Hill',
      childPlaces: []      
    }]
  }]
};

现在假设你要添加一个按钮来删除你已经访问过的地方。你会怎么做?更新嵌套状态 涉及从更改的部分一直向上复制对象。删除深度嵌套的位置将涉及复制其整个父位置链。这样的代码可能非常冗长。

¥Now let’s say you want to add a button to delete a place you’ve already visited. How would you go about it? Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose.

如果状态嵌套太多而不易更新,请考虑使其 “扁平”。这是你可以重组此数据的一种方法。你可以让每个地方保存其子地点 ID 的数组,而不是每个 place 都有其子地点数组的树状结构。然后存储一个从每个地点 ID 到对应地点的映射。

¥If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place.

这种数据重组可能会让你想起看到的数据库表:

¥This data restructuring might remind you of seeing a database table:

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },
  2: {
    id: 2,
    title: 'Africa',
    childIds: [3, 4, 5, 6 , 7, 8, 9]
  }, 
  3: {
    id: 3,
    title: 'Botswana',
    childIds: []
  },
  4: {
    id: 4,
    title: 'Egypt',
    childIds: []
  },
  5: {
    id: 5,
    title: 'Kenya',
    childIds: []
  },
  6: {
    id: 6,
    title: 'Madagascar',
    childIds: []
  }, 
  7: {
    id: 7,
    title: 'Morocco',
    childIds: []
  },
  8: {
    id: 8,
    title: 'Nigeria',
    childIds: []
  },
  9: {
    id: 9,
    title: 'South Africa',
    childIds: []
  },
  10: {
    id: 10,
    title: 'Americas',
    childIds: [11, 12, 13, 14, 15, 16, 17, 18],   
  },
  11: {
    id: 11,
    title: 'Argentina',
    childIds: []
  },
  12: {
    id: 12,
    title: 'Brazil',
    childIds: []
  },
  13: {
    id: 13,
    title: 'Barbados',
    childIds: []
  }, 
  14: {
    id: 14,
    title: 'Canada',
    childIds: []
  },
  15: {
    id: 15,
    title: 'Jamaica',
    childIds: []
  },
  16: {
    id: 16,
    title: 'Mexico',
    childIds: []
  },
  17: {
    id: 17,
    title: 'Trinidad and Tobago',
    childIds: []
  },
  18: {
    id: 18,
    title: 'Venezuela',
    childIds: []
  },
  19: {
    id: 19,
    title: 'Asia',
    childIds: [20, 21, 22, 23, 24, 25],   
  },
  20: {
    id: 20,
    title: 'China',
    childIds: []
  },
  21: {
    id: 21,
    title: 'India',
    childIds: []
  },
  22: {
    id: 22,
    title: 'Singapore',
    childIds: []
  },
  23: {
    id: 23,
    title: 'South Korea',
    childIds: []
  },
  24: {
    id: 24,
    title: 'Thailand',
    childIds: []
  },
  25: {
    id: 25,
    title: 'Vietnam',
    childIds: []
  },
  26: {
    id: 26,
    title: 'Europe',
    childIds: [27, 28, 29, 30, 31, 32, 33],   
  },
  27: {
    id: 27,
    title: 'Croatia',
    childIds: []
  },
  28: {
    id: 28,
    title: 'France',
    childIds: []
  },
  29: {
    id: 29,
    title: 'Germany',
    childIds: []
  },
  30: {
    id: 30,
    title: 'Italy',
    childIds: []
  },
  31: {
    id: 31,
    title: 'Portugal',
    childIds: []
  },
  32: {
    id: 32,
    title: 'Spain',
    childIds: []
  },
  33: {
    id: 33,
    title: 'Turkey',
    childIds: []
  },
  34: {
    id: 34,
    title: 'Oceania',
    childIds: [35, 36, 37, 38, 39, 40, 41],   
  },
  35: {
    id: 35,
    title: 'Australia',
    childIds: []
  },
  36: {
    id: 36,
    title: 'Bora Bora (French Polynesia)',
    childIds: []
  },
  37: {
    id: 37,
    title: 'Easter Island (Chile)',
    childIds: []
  },
  38: {
    id: 38,
    title: 'Fiji',
    childIds: []
  },
  39: {
    id: 40,
    title: 'Hawaii (the USA)',
    childIds: []
  },
  40: {
    id: 40,
    title: 'New Zealand',
    childIds: []
  },
  41: {
    id: 41,
    title: 'Vanuatu',
    childIds: []
  },
  42: {
    id: 42,
    title: 'Moon',
    childIds: [43, 44, 45]
  },
  43: {
    id: 43,
    title: 'Rheita',
    childIds: []
  },
  44: {
    id: 44,
    title: 'Piccolomini',
    childIds: []
  },
  45: {
    id: 45,
    title: 'Tycho',
    childIds: []
  },
  46: {
    id: 46,
    title: 'Mars',
    childIds: [47, 48]
  },
  47: {
    id: 47,
    title: 'Corn Town',
    childIds: []
  },
  48: {
    id: 48,
    title: 'Green Hill',
    childIds: []
  }
};

现在状态为 “扁平”(也称为 “规范化”),更新嵌套项变得更加容易。

¥Now that the state is “flat” (also known as “normalized”), updating nested items becomes easier.

为了现在删除一个地方,你只需要更新两个级别的状态:

¥In order to remove a place now, you only need to update two levels of state:

  • 其父位置的更新版本应从其 childIds 数组中排除已删除的 ID。

    ¥The updated version of its parent place should exclude the removed ID from its childIds array.

  • 根 “表格” 对象的更新版本应该包括父位置的更新版本。

    ¥The updated version of the root “table” object should include the updated version of the parent place.

下面是一个你可以如何去做的例子:

¥Here is an example of how you could go about it:

import { useState } from 'react';
import { initialTravelPlan } from './places.js';

export default function TravelPlan() {
  const [plan, setPlan] = useState(initialTravelPlan);

  function handleComplete(parentId, childId) {
    const parent = plan[parentId];
    // Create a new version of the parent place
    // that doesn't include this child ID.
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // Update the root state object...
    setPlan({
      ...plan,
      // ...so that it has the updated parent.
      [parentId]: nextParent
    });
  }

  const root = plan[0];
  const planetIds = root.childIds;
  return (
    <>
      <h2>Places to visit</h2>
      <ol>
        {planetIds.map(id => (
          <PlaceTree
            key={id}
            id={id}
            parentId={0}
            placesById={plan}
            onComplete={handleComplete}
          />
        ))}
      </ol>
    </>
  );
}

function PlaceTree({ id, parentId, placesById, onComplete }) {
  const place = placesById[id];
  const childIds = place.childIds;
  return (
    <li>
      {place.title}
      <button onClick={() => {
        onComplete(parentId, id);
      }}>
        Complete
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

你可以任意嵌套状态,但是将它设为 “扁平” 可以解决很多问题。它使状态更容易更新,并有助于确保你不会在嵌套对象的不同部分出现重复。

¥You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.

深入研究

提高内存使用率

¥Improving memory usage

理想情况下,你还可以从 “表格” 对象中删除已删除的项目(及其子项!)以提高内存使用率。这个版本就是这样做的。它还 使用 Immer 使更新逻辑更加简洁。

¥Ideally, you would also remove the deleted items (and their children!) from the “table” object to improve memory usage. This version does that. It also uses Immer to make the update logic more concise.

{
  "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": {}
}

有时,你还可以通过将一些嵌套状态移动到子组件中来减少状态嵌套。这适用于不需要存储的短暂 UI 状态,例如条目是否悬停。

¥Sometimes, you can also reduce state nesting by moving some of the nested state into the child components. This works well for ephemeral UI state that doesn’t need to be stored, like whether an item is hovered.

回顾

  • 如果两个状态变量总是一起更新,考虑将它们合并为一个。

    ¥If two state variables always update together, consider merging them into one.

  • 仔细选择状态变量以避免创建 “不可能的” 状态。

    ¥Choose your state variables carefully to avoid creating “impossible” states.

  • 以一种减少更新错误的可能性的方式来构建你的状态。

    ¥Structure your state in a way that reduces the chances that you’ll make a mistake updating it.

  • 避免冗余和重复状态,这样你就不需要保持同步。

    ¥Avoid redundant and duplicate state so that you don’t need to keep it in sync.

  • 除非你特别想阻止更新,否则不要将属性放入状态。

    ¥Don’t put props into state unless you specifically want to prevent updates.

  • 对于像选择这样的 UI 模式,将 ID 或索引保持在状态而不是对象本身。

    ¥For UI patterns like selection, keep ID or index in state instead of the object itself.

  • 如果更新深层嵌套状态很复杂,请尝试将其展平。

    ¥If updating deeply nested state is complicated, try flattening it.

挑战 1 / 4:
修复不更新的组件

¥Fix a component that’s not updating

这个 Clock 组件接收两个属性:colortime。当你在选择框中选择不同的颜色时,Clock 组件会从其父组件接收到不同的 color 属性。但是,由于某种原因,显示的颜色不会更新。为什么?解决问题。

¥This Clock component receives two props: color and time. When you select a different color in the select box, the Clock component receives a different color prop from its parent component. However, for some reason, the displayed color doesn’t update. Why? Fix the problem.

import { useState } from 'react';

export default function Clock(props) {
  const [color, setColor] = useState(props.color);
  return (
    <h1 style={{ color: color }}>
      {props.time}
    </h1>
  );
}


React 中文网 - 粤ICP备13048890号