本文由 简悦 SimpRead 转码, 原文地址 overreacted.io

当大家开始学习 React 时,常常问到风格指南。虽然呢,项目中应用一致的规则,是个不错的主意,但是很多规则挺随意的——所以 React 对这些并没有强烈的意见。

你可以使用不同的类型系统,使用函数声明或者箭头函数,也可以对你的属性按字母序或任何你愿意的其他顺序来排序。

这样的灵活性允许在项目中 整合 React 时,能应用已有的一些约定习惯。不过同时,这也会引发无休止的争论。

一些每个组件都应极力遵守的设计原则。但我不认为风格指南能很好地捕捉这些原则。接下来,我们先谈谈风格指南,然后再看看真的有用的设计原则

别被虚幻的问题分散了注意力

在我们讨论组件的设计原则前,我想先简单说说风格指南。这不是个流行的观点,但有人得说出来!

在 JavaScript 社区中,有一些由 linter 强制约束的样式指南。我个人的观察是,他们会产生比他们价值更多的麻烦。我算不清有多少人向我展示过一些完全有效的代码,然后跟我说 “React 提示这有问题啊!”,但这其实是由他们的 lint 配置产生的!这就导致了三个问题:

  • 人们习惯于将 linter 看作是一个过分热心又吵闹的看门人,而不是一个有用的工具。 有用的警告被风格提示的海洋淹没了。因此,人们在调试时不看 linter 的提示,错过有用的信息。此外,之前不太写 JavaScript 的人群(例如,设计人员)也因此更难使用代码。
  • 对于某种模式,大家不太学着区分有效和无效的用法。例如,有一条流行的规则是,禁止在 componentDidMount 中调用 setState。但如果这个用法总是 “错的”,那 React 根本不会允许它!这就有一个合法的用例,那就是测量 DOM 节点布局——例如,定位 tooltip。我见过有人添加 setTimeout 来 “解决” 这条规则,这完全搞错了。
  • 最终,人们采用 “执法者心态”,对那些没带来有意义变化但在代码中易于发现的地方持批评态度。“你用了函数声明,但我们的项目用的是箭头函数。” 每次我有强烈意愿,想要强制执行类似的规则时,仔细想想就会发现,我把个人情绪投入到了这个规则中——然后又努力让这消失。这让我陷入虚假的成就感,而丝毫没有改进我的代码。

我这是在宣扬停止使用 linter 吗? 不!

通过良好的配置,linter 是一个很好的工具,它可以在 bug 出现前就能发现它们。但它对代码风格的关注过多,使其变得会分散注意力。

整理你的 Lint 配置

这是我建议你在周一要做的事。把你的团队叫到一起花半个小时时间,一条条过一下你们项目中启用的 lint 规则,接着问问自己:“这条规则有帮我找到过 bug 吗?” 如果不是,关掉这条规则。(你也可以用 eslint-config-react-app 从头开始创建,里面不含任何代码风格的规则)

至少,你的团队应该有一个流程,会去删除引起干扰的规则项。不要假设一年前你或别人添加到你的 lint 配置中的任何东西,都是 “最佳实践”。保持质疑,找到答案。别让任何人告诉你,你不够聪明,不能选择 lint 规则。

那代码格式化呢?Prettier 然后忘掉 “风格”。你完全不需要一个工具告诉你得在这加一个空格,如果有别的工具能为你修复它。用 linter 找 bug,而不是做 代 码 美 学

当然,某些方面来说,编码风格和格式没有直接关系,但在整个项目中存在不一致时还是很恼人。

然而,它们中的许多都太微妙了,无法通过一条 lint 规则捕捉到。这就是为什么说,在团队成员之间建立信任,在 wiki 或简短的设计指南里分享有用的知识,是非常重要的事了。

不是一切都值得自动化!从 实际阅读 中获得的见解,这种指南中的理由可能比遵循 “规则” 更有价值。

如果遵循严格的风格指南是一种分心,那到底什么才是重要的呢?

这就是这篇文章的主题。

编写有弹性的组件

不论多少缩进或按字母序排列,都不能修复糟糕的设计。因此,我不会专注于某些代码看起来如何,而是专注于如何让它工作。这有一些组件设计原则我认为是非常有用:

  1. 不阻断数据流
  2. 时刻准备渲染
  3. 没有单例组件
  4. 隔离本地状态

即使你不使用 React,对于存在单向数据流的任何 UI 组件模型,可能也能通过反复试验发现相同的原则。

原则 1:不阻断数据流

渲染中不要阻断数据流

当别人使用你的组件时,他们的预期是,不论传递属性如何变化, 组件都将反映这些变化:

  1. <Button color={isOk ? 'blue' : 'red'} />

通常,这是 React 默认工作的方式。如果你在 Button 组件中使用 color,你会看到从上层为该渲染提供的值:

  1. function Button({ color, children }) {
  2. return (
  3. <button className={'Button-' + color}>
  4. {children}
  5. </button>
  6. );
  7. }

然而,学习 React 时常见的一个错误是,把 props 复制到 state:

  1. class Button extends React.Component {
  2. state = {
  3. color: this.props.color };
  4. render() {
  5. const { color } = this.state; return (
  6. <button className={'Button-' + color}>
  7. {this.props.children}
  8. </button>
  9. );
  10. }
  11. }

这也许看起来更直观,如果你使用过 React 之外的东西。 但是,通过将 prop 复制到 state,你忽略了对它的所有更新。

  1. <Button color={isOk ? 'blue' : 'red'} />

很少情况下,这样的行为 有意为之的,请确认将这样的属性取名为 initialColordefaultColor 来表明组件会忽略这个属性的改变。

但通常你会想在你的组件中 直接读取 props,避免复制 props(或从 props 中计算得到的值) 到 state:

  1. function Button({ color, children }) {
  2. return (
  3. <button className={'Button-' + color}>
  4. {children}
  5. </button>
  6. );
  7. }

计算值是另一个大家可能会将 props 复制到 state 的场景。举例来说,想象一下 按钮文字 的颜色是根据 color 属性通过昂贵计算得来:

  1. class Button extends React.Component {
  2. state = {
  3. textColor: slowlyCalculateTextColor(this.props.color) };
  4. render() {
  5. return (
  6. <button className={
  7. 'Button-' + this.props.color +
  8. ' Button-text-' + this.state.textColor }>
  9. {this.props.children}
  10. </button>
  11. );
  12. }
  13. }

这个组件有 bug,它在 color 属性改变时无法重新计算 this.state.textColor。最简单的修复是把 textColor 的计算放到 render 方法中,然后把组件改为 PureComponent

  1. class Button extends React.PureComponent { render() {
  2. const textColor = slowlyCalculateTextColor(this.props.color); return (
  3. <button className={
  4. 'Button-' + this.props.color +
  5. ' Button-text-' + textColor
  6. }>
  7. {this.props.children}
  8. </button>
  9. );
  10. }
  11. }

问题解决了!现在当 props 改变时重新计算 textColor,但是在属性不变时,能避免重复进行昂贵计算。

然而,也许我们还能再优化一下。如果 children 改变了呢?很遗憾 textColor 在这种情况下会重复计算。我们第二次尝试可能是通过在 componentDidUpdate 中调用计算。

  1. class Button extends React.Component {
  2. state = {
  3. textColor: slowlyCalculateTextColor(this.props.color)
  4. };
  5. componentDidUpdate(prevProps) { if (prevProps.color !== this.props.color) { this.setState({ textColor: slowlyCalculateTextColor(this.props.color), }); } } render() {
  6. return (
  7. <button className={
  8. 'Button-' + this.props.color +
  9. ' Button-text-' + this.state.textColor
  10. }>
  11. {this.props.children}
  12. </button>
  13. );
  14. }
  15. }

然而,这也意味着我们的组件在每次更新后,都有两次 render 调用。如果我们试图优化它,那也不理想。

你可以使用已不推荐的 componentWillReceiveProps 生命周期函数。然而,大家经常把 side effects 放这。这反过来又往往会给即将到来的并发渲染 特性像 Time Slicing 和 Suspense 带来问题。而更 “安全” 的 getDerivedStateFromProps 又有点难用。

让我们退一步。实际上,我们想要 memoization。我们有一些输入,除非输入发生变化,否则我们不想重新计算输出。

使用类,你可以使用帮助程序完成 memoization。但是,Hooks 更进了一步,提供了一种记忆昂贵计算的内置方法:

  1. function Button({ color, children }) {
  2. const textColor = useMemo( () => slowlyCalculateTextColor(color), [color] ); return (
  3. <button className={'Button-' + color + ' Button-text-' + textColor}>
  4. {children}
  5. </button>
  6. );
  7. }

这就是所有你需要的全部代码了!

在 class 组件中,你可以使用帮助程序类似 memoize-one。在函数组件中,useMemo Hook 提供了类似的功能。

现在我们看到优化昂贵的计算也不是将 props 复制到 state 的好理由。我们的渲染结果应该响应 props 的变化。

不要在 Side Effects 里阻断数据流

目前为止,我们讨论了关于如何在属性变化时保持渲染结果的一致。避免复制 props 到 state 是一部分。然而,side effects(例如获取数据)也是数据流重要的一部分

看看这个 React 组件:

  1. class SearchResults extends React.Component {
  2. state = {
  3. data: null
  4. };
  5. componentDidMount() { this.fetchResults(); } fetchResults() {
  6. const url = this.getFetchUrl();
  7. }
  8. getFetchUrl() {
  9. return 'http://myapi/results?query' + this.props.query;
  10. }
  11. render() {
  12. }
  13. }

很多 React 组件都像这样——但如果看看,会发现这有个 bug。fetchResults 方法使用了 query 属性来做数据获取:

  1. getFetchUrl() {
  2. return 'http://myapi/results?query' + this.props.query; }

但如果 query 变了呢?在这个组件中,什么都不会发生。这意味着组件的 side effects 没有响应它 props 的变化。这在 React 应用中是常见 bug 的根源。

为了修复这个组件,我们需要:


  • 查看 componentDidMount 和它所调用的所有方法
    • 在我们的例子中,就是 fetchResultsgetFetchUrl

  • 写下所有被他们使用的 props 和 state
    • 在我们的例子中,就是 this.props.query

  • 确保不论 props 如何变化,都重新执行 side effect
    • 我们可以通过 componentDidUpdate 方法实现
  1. class SearchResults extends React.Component {
  2. state = {
  3. data: null
  4. };
  5. componentDidMount() {
  6. this.fetchResults();
  7. }
  8. componentDidUpdate(prevProps) { if (prevProps.query !== this.props.query) { this.fetchResults(); } } fetchResults() {
  9. const url = this.getFetchUrl();
  10. }
  11. getFetchUrl() {
  12. return 'http://myapi/results?query' + this.props.query; }
  13. render() {
  14. }
  15. }

现在我们的代码能响应所有的 props 变化了,甚至是 side effects。

然而,牢记不要再搞砸了,这真是个挑战。例如,也许又加了 currentPage 到 state,然后在 getFetchUrl 中使用了它:

  1. class SearchResults extends React.Component {
  2. state = {
  3. data: null,
  4. currentPage: 0, };
  5. componentDidMount() {
  6. this.fetchResults();
  7. }
  8. componentDidUpdate(prevProps) {
  9. if (prevProps.query !== this.props.query) {
  10. this.fetchResults();
  11. }
  12. }
  13. fetchResults() {
  14. const url = this.getFetchUrl();
  15. }
  16. getFetchUrl() {
  17. return (
  18. 'http://myapi/results?query' + this.props.query +
  19. '&page=' + this.state.currentPage );
  20. }
  21. render() {
  22. }
  23. }

哎呀,我们的代码又有 bug 了,这是因为 side effect 没有响应 currentPage 的变化。

props 和 state 是 React 数据流的一部分。在这个数据流中,rendering 和 side effects 都应响应它们的变化,而不是忽略它们!

为了修复这个问题,可以重复上面的步骤:


  • 查看 componentDidMount 和它所调用的所有方法
    • 在我们的例子中,就是 fetchResultsgetFetchUrl

  • 写下所有被他们使用的 props 和 state
    • 在我们的例子中,就是 this.props.query **this.state.currentPage**

  • 确保不论 props 如何变化,都重新执行 side effect
    • 我们可以通过 componentDidUpdate 方法实现

让我们修复我们的组件,用以处理 currentPage 的更新吧:

  1. class SearchResults extends React.Component {
  2. state = {
  3. data: null,
  4. currentPage: 0,
  5. };
  6. componentDidMount() {
  7. this.fetchResults();
  8. }
  9. componentDidUpdate(prevProps, prevState) {
  10. if (
  11. prevState.currentPage !== this.state.currentPage || prevProps.query !== this.props.query
  12. ) {
  13. this.fetchResults();
  14. }
  15. }
  16. fetchResults() {
  17. const url = this.getFetchUrl();
  18. }
  19. getFetchUrl() {
  20. return (
  21. 'http://myapi/results?query' + this.props.query +
  22. '&page=' + this.state.currentPage );
  23. }
  24. render() {
  25. }
  26. }

如果我们能够以某种方式自动捕捉到这些错误,那不是很好吗?难道没有什么 linter 可以帮助我们处理这事吗?

遗憾的是,自动检查类组件的一致性太困难了。任何方法都可以调用任何其他方法。静态分析来自 componentDidMountcomponentDidUpdate 的调用充满了误报。

但是,可能 可以设计一个 被静态分析的一致性 API。React HookuseEffect 就是这样 API 的一个例子:

  1. function SearchResults({ query }) {
  2. const [data, setData] = useState(null);
  3. const [currentPage, setCurrentPage] = useState(0);
  4. useEffect(() => {
  5. function fetchResults() {
  6. const url = getFetchUrl();
  7. }
  8. function getFetchUrl() {
  9. return (
  10. 'http://myapi/results?query' + query + '&page=' + currentPage );
  11. }
  12. fetchResults();
  13. }, [currentPage, query]);
  14. }

我们将逻辑 放在 effect 中,这样可以更容易地看到它从 React 数据流中依赖了哪些值。这些值称为 “依赖”,在我们的例子中它们是 [currentPage, query]

注意这个 “effect 依赖” 数组并不是一个新概念。在类中,我们必须遍历所有方法调用,来搜索这些“依赖项”。useEffect API 只是显式地使用了相同的概念。

反过来说,这可以让我们自动验证它们:

编写有弹性的组件 - 图1

(这个 demo 来自最新推荐的 _exhaustive-deps_ lint 规则,这是 _eslint-plugin-react-hooks_ 其中一部分。它将不久加入到 Create React App 中)

注意,无论是将组件编写为类还是函数,都必须为 effect 响应所有 props 和 state 的更新。

使用 class API,你必须自己考虑一致性,并验证对每个相关 prop 或 state 的更改是否该由 componentDidUpdate 处理。否则,组件对 prop 和 state 的更改不具有弹性。这甚至不是专属于 React 的问题。它适用于任何允许你单独处理 “创建” 和 “更新” 事件的 UI 库

**useEffect** API 通过鼓励一致性来翻转默认值。可能在开始时觉得会不熟悉,但这样的结果是你的组件对逻辑更改更具弹性了。由于 “依赖关系” 现在是显式的,我们可以使用 lint 规则检验是否一致。 我们用 linter 来捕捉 bug!

不要在优化中阻断数据流

还有一种情况,你可能会意外忽略对 props 的更改。当你手动优化组件时,可能会发生这类错误。

注意,使用浅比较的优化方法(如 PureComponentReact.memo)与默认比较是安全的。

但是,如果你尝试通过编写自己的比较方法来 “优化” 组件,你可能会错误地忘记比较函数属性:

  1. class Button extends React.Component {
  2. shouldComponentUpdate(prevProps) { return this.props.color !== prevProps.color; } render() {
  3. const onClick = this.props.onClick; const textColor = slowlyCalculateTextColor(this.props.color);
  4. return (
  5. <button
  6. onClick={onClick}
  7. className={'Button-' + this.props.color + ' Button-text-' + textColor}>
  8. {this.props.children}
  9. </button>
  10. );
  11. }
  12. }

一开始很容易错过这个错误,因为对于类,你通常会传递一个方法,所以它会有相同的身份:

  1. class MyForm extends React.Component {
  2. handleClick = () => { } render() {
  3. return (
  4. <>
  5. <h1>Hello!</h1>
  6. <Button color='green' onClick={this.handleClick}> Press me </Button> </>
  7. )
  8. }
  9. }

所以我们的优化不会马上被破坏。但是,它将持续 “看到” 旧的 onClick 值,但其他 props 不会这样:

  1. class MyForm extends React.Component {
  2. state = {
  3. isEnabled: true
  4. };
  5. handleClick = () => {
  6. this.setState({ isEnabled: false });
  7. }
  8. render() {
  9. return (
  10. <>
  11. <h1>Hello!</h1>
  12. <Button color='green' onClick={ this.state.isEnabled ? this.handleClick : null }>
  13. Press me
  14. </Button>
  15. </>
  16. )
  17. }
  18. }

在这个例子中,应该禁用按钮点击事件——但这不会发生,因为 Button 组件忽略了对onClick 属性的任何更新。

如果函数标识本身依赖于可能随时间变化的东西,例如本例中的 draft.content,这可能会让人更加困惑:

  1. drafts.map(draft =>
  2. <Button
  3. color='blue'
  4. key={draft.id}
  5. onClick={
  6. this.handlePublish.bind(this, draft.content) }>
  7. Publish
  8. </Button>
  9. )

虽然 draft.content 可能随着时间的推移而改变,但是我们的 Button 组件忽略了对onClick 属性的改变,所以它继续看到 onClick 绑定方法的 “第一个版本” 与原始的draft.content

那怎么避免这个问题?

我建议避免手动实现 shouldComponentUpdate ,也要避免在 React.memo() 中使用自定义的比较方法。React.memo 中默认的浅比较会响应函数标识的更新:

  1. function Button({ onClick, color, children }) {
  2. const textColor = slowlyCalculateTextColor(this.props.color);
  3. return (
  4. <button
  5. onClick={onClick}
  6. className={'Button-' + color + ' Button-text-' + textColor}>
  7. {children}
  8. </button>
  9. );
  10. }
  11. export default React.memo(Button);

在类中,PureComponent 也有类似的行为.

这确保了传递不同的函数作为 prop,始终能有效工作。

如果你坚持使用自定义的比较,请确保不跳过函数:

  1. shouldComponentUpdate(prevProps) {
  2. return (
  3. this.props.color !== prevProps.color ||
  4. this.props.onClick !== prevProps.onClick );
  5. }

正如之前提到的,在类组件中很容易错过这个问题,因为方法标识通常是稳定的(但并非总是如此——而这就是 debug 困难的地方)。有了 Hooks,情况不同了:

  1. function 在每个渲染中都不同,所以你能马上发现这个问题
  2. 通过 useCallbackuseContext,你能 避免往下传递函数。这让你优化渲染时不用太担心函数的问题。

总结一下这部分,不要阻断数据流!

无论何时使用 props 和 state,请考虑如果它们发生变化会发生什么。在大多数情况下,组件不应以不同方式处理初始渲染和更新流程。这使它能够适应逻辑上的变化。

对于类,在生命周期方法中使用 props 和 state 时很容易忘记更新。Hooks 推动你做正确的事情——但是如果你不习惯于这样做,它会需要一些心理调整。

原则 2:时刻准备渲染

React 组件使你可以不用花太长时间就能编写渲染代码。你描述了在任何时刻 UI 应该 如何,接着 React 去完成。好好利用那个模型!

不要试图在组件行为中,假设任何不必要的时序信息。你的组件应该随时可以重新渲染。

违背这个原则的方式是什么样的?React 让这没那么容易发生——但你可以使用传统的componentWillReceiveProps 生命周期方法来实现它:

  1. class TextInput extends React.Component {
  2. state = {
  3. value: ''
  4. };
  5. componentWillReceiveProps(nextProps) { this.setState({ value: nextProps.value }); } handleChange = (e) => {
  6. this.setState({ value: e.target.value });
  7. };
  8. render() {
  9. return (
  10. <input
  11. value={this.state.value}
  12. onChange={this.handleChange}
  13. />
  14. );
  15. }
  16. }

在这个例子中,我们将 value 放在 state 里,但我们 从 props 接收 value。每当我们 “接收新 props” 时,就会重置状态中的 value

这种模式的问题在于它完全依赖于偶然的时间。

也许今天这个组件的父级很少更新,我们的 TextInput 仅在重要事件发生时 “接收 props”,比如保存表单。

但明天你可能会向 TextInput 的父组件加一些动画。如果父组件经常重渲染,它将 “污染” 子组件的状态!你可以在 “你可能不需要派生状态” 中阅读有关此问题的更多信息。

所以该怎么修复呢?

首先,我们得调整一下心理,需要停止将 “接收 props” 视为与 “渲染” 不同的东西。 由父组件引起的重渲染不应与由本地状态更改引起的重渲染不同。组件应该具有弹性,能适应更少或更频繁地渲染,否则它们与特定父组件存在过多耦合。

(这个 demo 给我们展示了重渲染会如何毁坏脆弱的组件)

当你真正想从 props 派生 state 时,尽管有一些不同解决方案,通常你应该使用一个完全受控制的组件:

  1. function TextInput({ value, onChange }) {
  2. return (
  3. <input
  4. value={value}
  5. onChange={onChange}
  6. />
  7. );
  8. }

或者使用一个不受控的组件,加上 key 来重置它:

  1. function TextInput() {
  2. const [value, setValue] = useState('');
  3. return (
  4. <input
  5. value={value}
  6. onChange={e => setValue(e.target.value)}
  7. />
  8. );
  9. }
  10. <TextInput key={formId} />

本节的内容是,组件不应该因为它或其父组件频繁地重渲染而坏掉。如果想避免使用传统的 componentWillReceiveProps 生命周期方法,React API 的设计,可以轻松实现这个目的。

要对你的组件进行压力测试,可以将这段代码临时添加到它的父组件:

  1. componentDidMount() {
  2. setInterval(() => this.forceUpdate(), 100);
  3. }

可别把这段代码保留在项目里——这只是用来检查父组件重渲染的频率超出预期时,会发生什么的快速方法。它不应该损坏子组件!

你可能会想:“那我在 props 改变时重置 state,再使用 PureComponent 来避免不必要的重渲染”。

这段代码应该能工作,对吧?

  1. class TextInput extends React.PureComponent { state = {
  2. value: ''
  3. };
  4. componentWillReceiveProps(nextProps) {
  5. this.setState({ value: nextProps.value });
  6. }
  7. handleChange = (e) => {
  8. this.setState({ value: e.target.value });
  9. };
  10. render() {
  11. return (
  12. <input
  13. value={this.state.value}
  14. onChange={this.handleChange}
  15. />
  16. );
  17. }
  18. }

乍一看,这个组件似乎解决了父级重渲染时 “污染” state 的问题。毕竟,如果 props 是相同的,我们会跳过更新——所以 componentWillReceiveProps 不会被调用。

但是这都是幻觉。这个组件对实际的 prop 更改仍然不具备弹性。例如,如果我们添加另一个经常变化的 prop,就像用来做动画的 style,我们还是会 “丢失” 内部状态:

  1. <TextInput
  2. style={{opacity: someValueFromState}} value={
  3. value
  4. }
  5. />

所以这个方法仍然存在缺陷。我们可以看到各种优化,例如 PureComponentshouldComponentUpdateReact.memo,它们不应该用于控制行为。只有提高性能的场景下,去使用它们。如果删除优化就会破坏某个组件,那么它就太脆弱了。

这里的解决方案和之前提到的一样。不要将 “接受 props” 视作特殊的事件。避免 “同步” props 和 state。大部分情况下,每个值都应该是完全控制的(通过 props),或者完全不受控制的(在本地 state 里)。可以的话,避免派生 state当然,时刻准备渲染!

原则 3:没有单例组件

有时我们假设某个组件只会显示一次,如导航栏。在一段时间内这也许是对的,然而,这种假设导致的设计问题,常常会在后期显现。

例如,你可能需要在路由变化后,两个 Page 组件切换实现动画——前一个 Page 和下一个 Page。它们都需要在动画期间被加载。但是,你可能会发现,每个组件都假定它是屏幕上唯一的 Page

要重现这类问题也很容易,试试渲染你的应用两次:

  1. ReactDOM.render(
  2. <>
  3. <MyApp /> <MyApp /> </>,
  4. document.getElementById('root')
  5. );

到处点击看看。(你可能需要为这个实验调整一些 CSS 样式)

你的应用仍然正常运行吗?或者你是否看到奇怪的崩溃和错误?偶尔对复杂组件进行压力测试是个好主意,可以确保组件存在多个拷贝时不会相互冲突。

我自己写过几次有问题的代码,例如在 componentWillUnmount 中执行全局状态 “清理”:

  1. componentWillUnmount() {
  2. this.props.resetForm();}

当然,如果页面上有两个这样的组件,卸载其中一个组件可能会破坏另一个组件。在 mount 后重置 “全局” 状态也不好:

  1. componentDidMount() {
  2. this.props.resetForm();}

在这情况下, 挂载 第二个 form 就会破坏前一个。

这些模式是检测组件是否脆弱的好指标。显示隐藏 一颗树,不应该破坏树之外的组件。

无论你是否有计划将这个组件渲染两次,从长远来看,解决这些问题是值得的。它将引导你进行更具弹性的设计。

原则 4:隔离本地状态

假设有一个 POST 社交组件,它有一个 Comment 列表(可扩展)和一个 NewComment 输入框。

React 组件可能有本地状态。但是哪个状态真的是自己的呢?帖子内容本身是否为本地状态?评论列表呢?或者评论流的记录?或评论框里输入的值?

如果你习惯于将所有内容都放入 “状态管理”,那么可能很难回答这个问题。所以这有一个简单的决定方式。

如果你不确定某个状态是否属于本地,请问自己:“如果此组件呈现两次,交互是否应反映在另一个副本中?” 只要答案为 “否”,那你就找到本地状态了。

例如,假设我们两次渲染相同的 Post。让我们看一下哪些内容会更新。

  • 发布内容 :我们希望在一棵树中编辑帖子,在另一棵树中也更新。因此,它可能不是 Post 组件的本地状态。(相反,帖子内容可能存在于 Apollo、Relay 或 Redux 等缓存中)
  • 评论列表 :这与帖子内容类似。我们希望在一棵树中添加一条新注释,以反映在另一棵树中。理想情况下,我们会为它使用某种缓存,而 不应该是 我们 Post 的本地状态。
  • 被展开的评论:如果在一棵树中展开评论也会在另一棵树中展开,这有点奇怪。在这种情况下,我们与特定的 Comment UI 元素 进行交互,而不是抽象的 “评论实体”。因此,“是否已展开” 应该Comment 的本地状态。
  • 输入的新评论的值:如果在一个输入框中输入评论,也会更新到另一个树中的输入框,这也有点奇怪。除非输入框明确地组合在一起,否则通常人们期望它们是独立的。所以输入值 应该NewComment 组件的本地状态。

我不建议对这些规则进行教条式的解释。当然,在一个简单的应用程序中,你可能希望使用本地状态来处理所有事情,包括 “缓存”。我只是谈论在第一原则下,理想状态的用户体验。

别把该本地的状态全局化了。这涉及到我们的 “弹性” 主题:组件之间发生的意外同步更少。作为奖励,这修复了一大类性能问题。当你的 state 在正确的地方时,“过度渲染” 都不成问题了。

回顾

让我们再一次回顾一下这些原则:

  1. 不阻断数据流 props 和 state 可能会更新,组件应该处理好这些更新,不论什么时候。
  2. 时刻准备渲染 一个组件不应该被或多或少的渲染而损坏。
  3. 没有单例组件 即使组件只渲染一次,但通过设计让它渲染两次也不会被破坏,是更好了。
  4. 隔离本地状态 想想哪个状态是特定 UI 展示下的本地状态——并且除非必要,不要将该状态提升到更高的地方。

这些原则能帮助你编写针对更新而优化的组件。这让添加,更改和删除那些组件更容易。

重要的是,一旦我们的组件具有了弹性,那就可以再回过头,到是否应按字母排序的紧迫困境中去了。