选择状态结构

良好地组织状态可以决定一个组件是易于修改和调试,还是不断产生 bug。以下是一些在组织状态时你应该考虑的建议。

You will learn

  • 何时使用单个状态变量与多个状态变量
  • 组织状态时应避免什么
  • 如何修复状态结构中的常见问题

状态结构的原则

当你编写一个保存某些状态的组件时,你需要决定使用多少个状态变量,以及它们的数据形状应该是什么。即使状态结构不够理想,也仍然可以写出正确的程序,但有一些原则可以帮助你做出更好的选择:

  1. 将相关状态组合在一起。 如果你总是同时更新两个或更多状态变量,考虑将它们合并为一个状态变量。
  2. 避免状态中的矛盾。 当状态被组织成多个部分可能互相矛盾、彼此“不一致”时,就会给错误留下空间。尽量避免这种情况。
  3. 避免冗余状态。 如果你可以在渲染期间从组件的 props 或其已有的状态变量计算出某些信息,就不应该把这些信息放进该组件的状态里。
  4. 避免状态重复。 当相同的数据在多个状态变量之间,或者在嵌套对象中被重复保存时,就很难保持它们同步。尽可能减少重复。
  5. 避免深层嵌套状态。 过深的层级状态不太方便更新。尽可能将状态结构设计得扁平一些。

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

现在让我们看看这些原则如何在实践中发挥作用。

你有时可能会在使用单个状态变量还是多个状态变量之间犹豫不决。

你应该这样做吗?

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

还是这样?

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

从技术上讲,这两种方式都可以。但如果某两个状态变量总是一起变化,那么把它们统一到一个状态变量中可能是个好主意。 这样你就不会忘记始终让它们保持同步,例如在这个例子中,移动鼠标会更新红点的两个坐标:

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

另一种将数据组合成对象或数组的情况是,你不知道自己需要多少个状态。比如,当你有一个表单,用户可以添加自定义字段时,这样做就很有帮助。

Pitfall

如果你的状态变量是一个对象,请记住,你不能只更新其中一个字段 而不显式复制其他字段。例如,在上面的例子中,你不能直接写 setPosition({ x: 100 }),因为那样就根本没有 y 属性了!相反,如果你只想设置 x,要么写 setPosition({ ...position, x: 100 }),要么把它们拆成两个状态变量,然后写 setX(100)

避免状态中的矛盾

下面是一个酒店反馈表单,其中有 isSendingisSent 两个状态变量:

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>感谢你的反馈!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>你在 The Prancing Pony 住得怎么样?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        发送
      </button>
      {isSending && <p>正在发送...</p>}
    </form>
  );
}

// 假装发送一条消息。
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

虽然这段代码可以工作,但它为“不可能”的状态留下了空间。例如,如果你忘记同时调用 setIsSentsetIsSending,就可能出现 isSendingisSent 同时为 true 的情况。组件越复杂,就越难理解发生了什么。

由于 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>感谢你的反馈!</h1>
  }

  return (
    <form onSubmit={handleSubmit}>
      <p>你在 The Prancing Pony 住得怎么样?</p>
      <textarea
        disabled={isSending}
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <br />
      <button
        disabled={isSending}
        type="submit"
      >
        发送
      </button>
      {isSending && <p>正在发送...</p>}
    </form>
  );
}

// 假装发送一条消息。
function sendMessage(text) {
  return new Promise(resolve => {
    setTimeout(resolve, 2000);
  });
}

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

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

但它们不是状态变量,所以你不需要担心它们彼此不同步。

避免冗余状态

如果你可以在渲染期间从组件的 props 或其已有的状态变量计算出某些信息,你不应该把这些信息放进该组件的状态里。

例如,看看这个表单。它可以工作,但你能找出其中有任何冗余状态吗?

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>让我们为你办理入住</h2>
      <label>
        名字:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        姓氏:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        你的票将签发给:<b>{fullName}</b>
      </p>
    </>
  );
}

这个表单有三个状态变量:firstNamelastNamefullName。然而,fullName 是冗余的。你总是可以在渲染期间根据 firstNamelastName 计算出 fullName,所以把它从状态中移除。

你可以这样做:

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>让我们为你办理入住</h2>
      <label>
        名字:{' '}
        <input
          value={firstName}
          onChange={handleFirstNameChange}
        />
      </label>
      <label>
        姓氏:{' '}
        <input
          value={lastName}
          onChange={handleLastNameChange}
        />
      </label>
      <p>
        你的票将签发给:<b>{fullName}</b>
      </p>
    </>
  );
}

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

const fullName = firstName + ' ' + lastName;

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

Deep Dive

不要在 state 中镜像 props

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

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

这里,color 状态变量被初始化为 messageColor prop。问题在于,如果父组件之后传入了不同的 messageColor 值(例如从 'blue' 变成 'red'),color 状态变量 不会被更新! 这个状态只会在第一次渲染时初始化。

这就是为什么将某个 prop “镜像”到状态变量中会让人困惑。相反,请在代码中直接使用 messageColor prop。如果你想给它一个更短的名字,可以使用一个常量:

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

这样它就不会和父组件传入的 prop 不同步了。

只有当你想要忽略某个特定 prop 的所有更新时,把 props “镜像”到 state 才有意义。按照约定,可以在 prop 名称前加上 initialdefault,以明确表示它的新值会被忽略:

function Message({ initialColor }) {
// `color` 状态变量保存 `initialColor` 的 *第一个* 值。
// 对 `initialColor` prop 后续的变化会被忽略。
const [color, setColor] = useState(initialColor);

避免状态中的重复

这个菜单列表组件可以让你从多个旅行零食中选择一个:

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 列表中的某一项是同一个对象。 这意味着关于该项目本身的信息在两个地方被重复保存了。

为什么这会有问题?我们把每一项都改成可编辑的:

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

请注意,如果你先点击某一项上的“Choose”,然后再编辑它,输入框会更新,但底部的标签不会反映这些编辑。 这是因为你有重复状态,而且你忘记更新 selectedItem 了。

虽然你也可以同时更新 selectedItem,但更简单的修复方式是移除重复。这个例子中,不再使用一个 selectedItem 对象(它与 items 里的对象重复),而是在状态中保存 selectedId,然后通过在 items 数组里查找该 ID 对应的项来得到 selectedItem

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

之前的状态是这样重复的:

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

但修改后就变成这样:

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

重复消失了,你只保留了必要的状态!

现在如果你编辑被选中的项目,下方消息会立即更新。这是因为 setItems 会触发重新渲染,而 items.find(...) 会找到标题已更新的项目。你不需要在状态中保留被选中的项目,因为只有被选中的 ID是必要的。其余内容都可以在渲染时计算出来。

避免深层嵌套的状态

想象一个由行星、大洲和国家组成的旅行计划。你可能会想用嵌套的对象和数组来组织它的状态,就像这个例子:

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

现在假设你想添加一个按钮,用来删除你已经访问过的某个地点。你会怎么做?更新嵌套状态 需要从变化的那一部分一直向上复制对象。删除一个深层嵌套的地点,意味着要复制它整个父级链。这类代码会非常冗长。

如果状态嵌套得太深,导致难以更新,可以考虑把它变“平”。 下面是一种重构这些数据的方法。与其使用树形结构,让每个 place 持有一个包含其子地点的数组,不如让每个地点持有一个包含其子地点 ID 的数组。然后再存一个从每个地点 ID 到对应地点的映射。

这种数据重构可能会让你联想到数据库表:

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

现在状态是“平”的了(也叫“规范化”),更新嵌套项就更容易了。

为了删除一个地点,你现在只需要更新两层状态:

  • 它的父级地点的更新版本,应该从 childIds 数组中移除被删除的 ID。
  • 根“表”对象的更新版本,应该包含父级地点的更新版本。

下面是一个实现示例:

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];
    // 创建父级地点的一个新版本,
    // 其中不包含这个子 ID。
    const nextParent = {
      ...parent,
      childIds: parent.childIds
        .filter(id => id !== childId)
    };
    // 更新根状态对象……
    setPlan({
      ...plan,
      // ……这样它就拥有更新后的父级。
      [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);
      }}>
        完成
      </button>
      {childIds.length > 0 &&
        <ol>
          {childIds.map(childId => (
            <PlaceTree
              key={childId}
              id={childId}
              parentId={id}
              placesById={placesById}
              onComplete={onComplete}
            />
          ))}
        </ol>
      }
    </li>
  );
}

你可以按任意深度嵌套状态,但把它变“平”可以解决很多问题。这样会让状态更容易更新,也有助于确保你不会在嵌套对象的不同部分中产生重复数据。

Deep Dive

改善内存使用

理想情况下,你还应该把已删除的项目(以及它们的子项!)从“表”对象中移除,以改善内存使用。这个版本就是这么做的。它还 使用了 Immer 来让更新逻辑更简洁。

{
  "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 状态非常有效,比如某一项是否处于悬停状态。

Recap

  • 如果两个状态变量总是一起更新,可以考虑把它们合并成一个。
  • 仔细选择你的状态变量,避免创建“不可能”的状态。
  • 以一种能降低更新出错概率的方式组织状态。
  • 避免冗余和重复状态,这样你就不需要让它们保持同步。
  • 不要把 props 放进 state,除非你明确想阻止更新。
  • 对于选择这类 UI 模式,状态里保留 ID 或索引,而不是对象本身。
  • 如果更新深层嵌套状态很复杂,尝试把它扁平化。

Challenge 1 of 4:
修复一个没有更新的组件

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

import { useState } from 'react';

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