选择状态结构

良好地结构化状态可以决定组件是易于修改和调试,还是持续产生错误的源头。在结构化状态时,你应该考虑以下一些建议。

🌐 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.

你将学习到

  • 何时使用单个与多个状态变量
  • 组织状态时要避免什么
  • 如何解决状态结构的常见问题

结构化状态的原则

🌐 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. 归类相关状态。 如果你总是同时更新两个或更多状态变量,考虑将它们合并为一个状态变量。
  2. 避免状态中的矛盾。 当状态的结构使得多块状态可能互相矛盾和“不一致”时,你就为错误留出了空间。尽量避免这种情况。
  3. 避免冗余状态。 如果你可以在渲染期间从组件的 props 或现有的状态变量计算出某些信息,那么你就不应该将这些信息放入组件的状态中。
  4. 避免在状态中重复。 当相同的数据在多个状态变量之间,或在嵌套对象中重复时,很难保持它们的同步。尽量减少重复。
  5. 避免深度嵌套状态。 深层次的分层状态更新起来不太方便。尽可能的话,最好将状态结构化为平面形式。

这些原则背后的目标是使状态易于更新而不会引入错误。从状态中移除冗余和重复的数据有助于确保其各部分保持同步。这类似于数据库工程师可能希望通过“规范化”数据库结构 来减少错误的机会。用阿尔伯特·爱因斯坦的话来说,“让你的状态尽可能简单——但不能再简单。”

🌐 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'

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

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

🌐 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.

这就是为什么在状态变量中“镜像”某个 prop 可能会导致混淆的原因。相反,应在代码中直接使用 messageColor prop。如果你想给它一个更短的名字,可以使用常量:

🌐 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.

将“props”映射到状态只有在你想要忽略某个特定 prop 的所有更新时才有意义。按照惯例,可以在 prop 名称前加上 initialdefault 来明确表示其新值会被忽略:

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,但一个更简单的解决办法是去除重复。在这个例子中,不是使用一个 selectedItem 对象(它会与 items 内的对象产生重复),你将 selectedId 保存在状态中,然后通过在 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.

如果状态嵌套过深而不容易更新,考虑将其“扁平化”。 这里有一种重组这些数据的方法。不要使用树状结构,每个 place 都有一个其子地点的数组,而是让每个地点保存一个其子地点 ID的数组。然后存储一个从每个地点 ID 到对应地点的映射。

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

🌐 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: []
  }
};

现在状态是“扁平的”(也称为“标准化”)时,更新嵌套项变得更容易。

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

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

  • 地点的更新版本应从其 childIds 数组中排除已移除的 ID。
  • 根“表”对象的更新版本应包括父位置的更新版本。

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

🌐 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

理想情况下,你还应该从 “table” 对象中移除已删除的项目(以及它们的子项!)以提高内存使用效率。这一版本就是这样做的。它还使用 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.

回顾

  • 如果两个状态变量总是一起更新,考虑将它们合并为一个。
  • 仔细选择你的状态变量,以避免创建“不可能”的状态。
  • 以一种减少更新错误的可能性的方式来构建你的状态。
  • 避免冗余和重复状态,这样你就不需要保持同步。
  • 除非你特别想阻止更新,否则不要把 props 放入 state。
  • 对于像选择这样的 UI 模式,将 ID 或索引保持在状态而不是对象本身。
  • 如果更新深层嵌套状态很复杂,请尝试将其展平。

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

🌐 Fix a component that’s not updating

这个 Clock 组件接收两个 props:colortime。当你在下拉框中选择不同的颜色时,Clock 组件会从其父组件接收到不同的 color prop。然而,不知为何,显示的颜色没有更新。为什么?修复这个问题。

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