为什么这是 React 中的“反模式”???

2025-06-08

为什么这是 React 中的“反模式”???

刚开始学习 React 时,我有一些困惑。事实上,我认为几乎所有用过 React 的人都会遇到同样的问题。我知道这一点,因为人们已经构建了完整的库来解决这些问题。其中两个主要问题,几乎困扰着每一个 React 开发者:

  1. “一个组件如何访问驻留在另一个组件中的信息(特别是状态变量)?”

  2. “一个组件如何调用另一个组件中的函数?”

总体而言,JavaScript 开发者(尤其是 React 开发者)近年来越来越专注于编写“纯”函数。这些函数不与状态变化交织在一起。这些函数不需要外部数据库连接。这些函数不需要了解函数外部发生的任何事情。

专注于“纯”函数无疑是一个崇高的目标。但是,如果你构建的是任何规模和范围都合理的应用程序,就不可能让每个函数都“纯”。在某种程度上,如果一个应用程序的某些组件本身无法感知其他组件的功能,那么构建这样的应用程序就变得荒谬了。这些相互关联的环节通常被称为依赖关系

一般来说,依赖关系并非好事,明智的做法是只在必要时引入它们。但同样,如果你的应用发展到“一定规模”,那么至少有一些组件不可避免地会相互依赖。当然,React 开发者深知这一点,因此他们提供了一种基本方法,让组件可以将关键信息或函数传递给其子组件。

通过 Props 传递值的默认方法

任何状态值都可以通过 props 传递给另一个组件。任何函数都可以通过相同的 props 向下传递。这使得子组件能够“感知”存储在更高层级的状态值。并且,这也使得它们能够调用父组件的操作。这一切都很好。但不久之后,React 新手开发者就会开始担心一个具体的潜在“问题”。

大多数应用在构建时都具有一定程度的“分层”。在大型应用中,这种分层可能会嵌套得相当深。常见的架构可能如下所示:

  1. <App>→ 呼叫 →<ContentArea>
  2. <ContentArea>→ 呼叫 →<MainContentArea>
  3. <MainContentArea>→ 呼叫 →<MyDashboard>
  4. <MyDashboard>→ 呼叫 →<MyOpenTickets>
  5. <MyOpenTickets>→ 呼叫 →<TicketTable>
  6. <TicketTable>→ 调用一系列 → <TicketRow>s
  7. 每个<TicketRow>→ 调用 →<TicketDetail>

理论上,这种菊花链可以延伸到更多层级。所有组件都是一个连贯整体的组成部分。具体来说,它们构成了一个层级结构。但关键问题是:

在上面的例子中,<TicketDetail>组件可以读取 中的状态值<ContentArea>吗?或者……<TicketDetail>组件可以调用 中的函数吗<ContentArea>


这两个问题的答案都是肯定的。理论上,所有后代组件都能感知到其祖先组件中存储的所有变量。并且它们可以调用祖先组件的函数——但有一个重要的警告:为了实现这一点,这些值(状态值或函数)必须显式地作为 props传递下去。如果不是这样,那么后代组件就无法感知祖先组件中可用的状态值或函数。

在小型应用或实用程序中,这可能不是什么大问题。例如,如果<TicketDetail>需要查询位于 中的状态变量<TicketRow>,只需确保<TicketRow>→ 将这些值通过一个或多个 props 传递给 →即可。如果需要在 上调用函数,<TicketDetail>也是如此→ 只需将该函数作为 prop 传递给 → 即可。当树下某个组件需要与位于层级结构中更高层级的状态/函数交互时,就会出现令人头疼的问题。<TicketDetail><TicketRow><TicketRow><TicketDetail>

React 解决这个问题的“传统”方法是将变量/函数在整个层级结构中向下传递。但这会产生大量繁琐的开销,并且需要大量的认知规划。要以 React 的“默认”方式实现这一点,我们必须将值传递到多个不同的层级,如下所示:

<ContentArea><MainContentArea><MyDashboard><MyOpenTickets><TicketTable><TicketRow><TicketDetail>

为了从一直到获得一个状态变量,我们做了大量额外工作。大多数高级开发人员很快就会意识到,这会创建一个长得离谱的值和函数链,它们会不断地通过 props 传递,经过层层递进的组件。这个解决方案感觉非常笨重,以至于在我最初几次尝试深入 React 库的时候,它甚至让我放弃了学习。<ContentArea><TicketDetail>

名为Redux的巨型怪兽

我并非唯一一个认为通过 props 传递所有共享状态值和所有共享函数非常不切实际的人。我之所以知道这一点,是因为几乎不可能找到一个大型的 React 实现不使用所谓的附加附加组件(即状态管理工具)的。市面上有很多这样的工具。我个人很喜欢MobX。但不幸的是,“行业标准”是 Redux。

Redux 是由构建核心 React 库的同一支团队创建的。换句话说,React 团队打造了这个精美的工具。但他们几乎立刻就意识到,该工具固有的状态共享方法几乎难以管理。因此,如果他们找不到某种方法来克服这个(原本精美的)工具的固有障碍,它就永远不会被广泛采用。

因此他们创建了 Redux。

Redux 就像是 React 蒙娜丽莎脸上的胡子。它需要将大量的样板代码转储到几乎所有项目文件中。这让故障排除和代码阅读变得更加困难。它把宝贵的业务逻辑发送到遥远的文件中。这真是一团糟。

但是,如果一个团队面临使用 React + Redux 的前景,或者完全不使用第三方状态管理工具,他们几乎总是会选择 React + Redux。此外,由于 Redux 是由 React 核心团队构建的,因此它带有这种默许的认可。大多数开发团队更倾向于选择任何拥有这种默许认可的解决方案。

当然,Redux 也会在你的 React 应用中创建底层的依赖关系网络。但公平地说,任何通用的状态管理工具都会做同样的事情。状态管理工具充当一个公共存储,我们可以在其中保存变量和函数。任何有权访问该公共存储的组件都可以使用这些变量和函数。唯一明显的缺点是,现在每个组件都依赖于该公共存储。

我认识的大多数 React 开发者都放弃了最初对 Redux 的抵触情绪。(毕竟……抵触是徒劳的。)我见过很多彻底讨厌Redux 的人,但面对着使用 Redux 的前景——或者失去一份 React 工作——他们吞下了索玛,喝下了酷爱饮料,现在他们终于接受了 Redux 是生活中不可或缺的一部分。就像纳税、直肠检查和根管治疗一样。

重新思考 React 中的共享价值

我总是有点固执己见。我看了一眼 Redux,就知道我必须寻找更好的解决方案。我用 Redux,我也曾在用过它的团队工作过,我理解它的作用。但这并不意味着我喜欢这份工作的这方面。

正如我已经说过的,如果绝对需要一个单独的状态管理工具,那么 MobX 比 Redux 好上一百万倍。但是,关于 React 开发者的集体思维,还有一个更深层次的问题困扰着我:

为什么我们一开始就不断寻求国家管理工具?


你知道,当我刚开始 React 开发时,我在家里花了好几个晚上尝试各种替代方案。而我找到的解决方案似乎遭到了许多其他 React 开发者的嘲笑——但他们也说不出个所以然。让我来解释一下:

在上面概述的假定应用程序中,假设我们创建一个如下所示的单独文件:

// components.js
let components = {};
export default components;
Enter fullscreen mode Exit fullscreen mode

就这样。只需两行代码。我们创建一个空对象——一个普通的 JavaScript 对象export default。然后我们在文件中将其设置为。

现在让我们看看组件内部的代码是什么样的<ContentArea>

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      components.ContentArea = this;
   }

   consoleLog(value) {
      console.log(value);
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

总体来说,这看起来像一个相当“普通”的基于类的 React 组件。我们有一个简单的render()函数,它调用层次结构中它下面的下一个组件。我们有一个小小的演示函数,它除了向 发送一些值之外什么也不做console.log(),还有一个构造函数。但是……这个构造函数里有一些不同之处

在文件顶部,请注意我们导入了那个非常简单的components对象。然后,在构造函数中,我们为该components对象添加了一个与 React 组件同名的新属性this。在该属性中,我们加载了对 React 组件的引用this。所以……从现在开始,任何时候我们访问该components对象,我们也可以直接访问<ContentArea>组件。

现在让我们深入到层次结构的底部<TicketDetail>,看看它是什么样子的:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      components.ContentArea.consoleLog('it works');
      return <div>Here are the ticket details.</div>;
   }
}
Enter fullscreen mode Exit fullscreen mode

事情是这样的。每次<TicketDetail>组件渲染时,它都会调用组件consoleLog()存在的函数注意函数并没有通过 props 在整个层级链中传递。实际上,该函数根本没有传递任何组件。<ContentArea>consoleLog()consoleLog()

然而,<TicketDetail>仍然能够调用的<ContentArea>功能consoleLog(),因为两个必要的步骤已经完成:

  1. <ContentArea>组件被加载时,它会将对自身的引用添加到共享components对象中。

  2. <TicketDetail>组件被加载时,它导入共享components对象,这意味着它可以直接访问组件<ContentArea>,即使的属性从未通过 props<ContentArea>传递。<TicketDetail>

这不仅适用于函数/回调。它还可以用于直接查询状态变量的值。假设它<ContentArea>看起来像这样:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

那么我们可以<TicketDetail>这样写:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return <div>Here are the ticket details.</div>;
   }
}
Enter fullscreen mode Exit fullscreen mode

所以现在,每次<TicketDetail>渲染 时,它都会查看 变量的值<ContentArea>state.reduxSucks如果值为true,它就会console.log()读取消息。即使 的值ContentArea.state.reduxSucks从未通过 props 传递给任何组件,它也能做到这一点。通过利用一个简单的、“存在于”标准 React 生命周期之外的 JavaScript 基础对象,我们现在可以让任何子组件直接从已加载到该对象中的任何父组件读取状态变量components。我们甚至可以使用它在子组件中调用父组件的函数。

因为我们可以直接调用祖先组件中的函数,这意味着我们甚至可以直接从子组件影响父组件的状态值。我们可以这样做:

首先,在<ContentArea>组件中,我们创建一个简单的函数来切换的值reduxSucks

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

然后,在<TicketDetail>组件中,我们使用components对象来调用该方法:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

现在,每次<TicketDetail>渲染组件时,它都会向用户显示一个按钮。点击该按钮实际上会ContentArea.state.reduxSucks实时更新(切换)变量的值。即使ContentArea.toggleReduxSucks()函数从未通过 props 传递,它也能做到这一点。

我们甚至可以使用这种方法,允许祖先组件直接调用其后代组件上的函数。具体方法如下:

更新后的<ContentArea>组件将如下所示:

// content.area.js
import components from './components';
import MainContentArea from './main.content.area';
import React from 'react';

export default class ContentArea extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucks:true };
      components.ContentArea = this;
   }

   toggleReduxSucks() {
      this.setState((previousState, props) => {
         return { reduxSucks: !previousState.reduxSucks };
      });
      components.TicketTable.incrementReduxSucksHasBeenToggledXTimes();
   }

   render() {
      return <MainContentArea/>;
   }
}
Enter fullscreen mode Exit fullscreen mode

现在我们将在<TicketTable>组件中添加如下逻辑:

// ticket.table.js
import components from './components';
import React from 'react';
import TicketRow from './ticket.row';

export default class TicketTable extends React.Component {
   constructor(props) {
      super(props);
      this.state = { reduxSucksHasBeenToggledXTimes: 0 };
      components.TicketTable = this;
   }

   incrementReduxSucksHasBeenToggledXTimes() {
      this.setState((previousState, props) => {
         return { reduxSucksHasBeenToggledXTimes: previousState.reduxSucksHasBeenToggledXTimes + 1};
      });      
   }

   render() {
      const {reduxSucksHasBeenToggledXTimes} = this.state;
      return (
         <>
            <div>The `reduxSucks` value has been toggled {reduxSucksHasBeenToggledXTimes} times</div>
            <TicketRow data={dataForTicket1}/>
            <TicketRow data={dataForTicket2}/>
            <TicketRow data={dataForTicket3}/>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

最后,我们的<TicketDetail>组件保持不变。它仍然看起来像这样:

// ticket.detail.js
import components from './components';
import React from 'react';

export default class TicketDetail extends React.Component {
   render() {
      if (components.ContentArea.state.reduxSucks === true) {
         console.log('Yep, Redux is da sux');
      }
      return (
         <>
            <div>Here are the ticket details.</div>
            <button onClick={() => components.ContentArea.toggleReduxSucks()}>Toggle reduxSucks</button>
         </>
      );
   }
}
Enter fullscreen mode Exit fullscreen mode

现在,你可能会发现这三个类有些奇怪。在我们的应用程序层次结构中,<ContentArea>→ 是 → → 的祖先<TicketTable>,而 → → 又是 → 的祖先<TicketDetail>。这意味着,当<ContentArea>被挂载时,它(最初)对 一无所知<TicketTable>。然而,在<ContentArea>toggleReduxSucks()函数内部,却隐式调用了后代的函数:incrementReduxSucksHasBeenToggledXTimes()。所以这会破坏对吗

嗯...不。

你看,考虑到我们在应用中创建的层,应用中只有一条“路径”toggleReduxSucks()可以调用。它就像这样:

  1. <ContentArea>已安装并渲染。

  2. 在此过程中,对的引用<ContentArea>被加载到components对象中。

  3. 这最终导致<TicketTable>被安装和渲染。

  4. 在此过程中,对的引用<TicketTable>被加载到components对象中。

  5. 这最终导致<TicketDetail>被安装和渲染。

  6. 然后向用户显示“Toggle reduxSucks” <button>

  7. 用户点击“Toggle reduxSucks” <button>

  8. 这将调用组件toggleReduxSucks()中的函数<ContentArea>

  9. 这反过来又调用了组件incrementReduxSucksHasBeenToggledXTimes()中的函数<TicketTable>

  10. 之所以有效,是因为当用户点击“Toggle reduxSucks”按钮时<button>,该组件的引用<TicketTable>已经被加载到components对象中。当<ContentArea>toggleReduxSucks()函数被调用时,它能够在对象中找到<TicketTable>对 的incrementReduxSucksHasBeenToggledXTimes()函数的引用components

所以你看,通过利用我们应用程序的固有层次结构,我们可以在<ContentArea>组件中放置逻辑,该逻辑将有效地调用其后代组件之一中的函数,即使该组件在安装时<ContentArea>尚未意识到该<TicketTable>组件。

抛弃你的状态管理工具

正如我已经解释过的,我深信MobX远胜Redux。每当我有幸(难得)参与“绿地”项目时,我总会极力游说我们使用 MobX 而不是 Redux。但当我构建自己的应用时,我很少(甚至从未)使用任何第三方状态管理工具。相反,我经常在合适的地方使用这种超级简单的对象/组件缓存机制。当这种方法无法满足需求时,我常常会回归到 React 的“默认”解决方案——换句话说,我只是通过 props 传递函数/状态变量。

此方法的已知“问题”

我并不是说使用基本缓存的想法components是解决所有共享状态/函数问题的万能方案。有时候,这种方法可能……很棘手,甚至完全错误。以下是一些需要考虑的重要问题:

  • 这最适合单例例如,在上面显示的层次结构中,组件内部
    有零到多个组件。如果您想将每个潜在组件(及其子组件)的引用缓存到缓存中,则必须将它们存储在一个数组中,这肯定会变得……令人困惑。我一直避免这样做。<TicketRow><TicketTable><TicketRow><TicketDetail>components

  • 缓存components(显然)的工作原理是,除非我们知道其他组件的变量/函数已加载到components对象中,否则我们无法利用它们。
    如果您的应用程序架构使其不切实际,那么这可能是一个糟糕的解决方案。这种方法非常适合单页应用程序,因为我们可以确定地知道<AncestorComponent> 始终会在 之前挂载。如果您选择直接从 中的某个位置引用<DescendantComponent>中的变量/函数,则必须确保应用程序流程在已加载到缓存中之前不允许发生这种情况<DescendantComponent><AncestorComponent><DescendantComponent>components

  • 虽然您可以从缓存中引用的其他组件读取components状态变量,但是如果您想更新这些变量(通过setState()),则必须调用setState()其关联组件中的函数。

买者自慎

现在我已经演示了这种方法,并概述了一些已知的限制,我觉得有必要强调一个重要的注意事项。自从我“发现”了这种方法以来,我已经在不同的场合与那些自认为是认证“React 开发者”的人分享过它。每次我告诉他们,他们总是给我同样的回应:

嗯...不要那样做。


他们皱着鼻子,皱着眉头,看起来就像我刚放了个大屁一样。这种方法似乎让很多“React 开发者”觉得……不对劲。诚然,我还没听到有人给我任何实证证据来证明它(据说)“不对劲”。但这并不能阻止他们把它当成……一种罪过

所以,即使你喜欢这种方法,或者你觉得它在某些情况下很“方便”,我也不建议在 React 职位的面试中提起它。事实上,即使你只是在和其他“React 开发者”交谈,我也会谨慎考虑你是否应该提及它。

你看,我发现 JS 开发者——尤其是 React 开发者——有时非常固执己见。有时他们会给出经验性的理由,说明为什么方法 A 是“错误的”,而方法 B 是“正确的”。但更多时候,他们只是简单地看了一段代码,就宣称它“不好”——即使他们无法给出任何实质性的理由来支持他们的观点。

为什么这种方法会让大多数“React Devs”感到厌烦呢?

如上所述,当我实际向其他 React 同事展示这个方法时,我还没有收到任何合理的回应,解释为什么这种方法“不好”。但当我得到解释时它往往会落入以下(几个)借口之一:

  • 这违背了“纯”函数的初衷,并导致应用程序充斥着紧密耦合的依赖关系。
    好吧……我明白了。但那些立即拒绝这种方法的人,却乐于将 Redux(或 MobX,或任何状态管理工具)放入几乎所有 React 类/函数的中间。现在,我并非反对状态管理工具有时绝对有益的普遍观点。但每个状态管理工具本质上都是一个巨大的依赖生成器。每次将状态管理工具放入函数/类中间,实际上就是在应用程序中堆满依赖关系。请注意:我并没有说你应该将每个函数/类都放入components缓存中。事实上,你可以仔细选择将哪些函数/类放入components缓存中,以及哪些函数/类尝试引用已放入components缓存的内容。如果你正在编写一个纯粹的实用函数/类,那么利用我的缓存解决方案可能是一个非常糟糕的主意components。因为使用components缓存需要“了解”应用中的其他组件。如果你正在编写需要在应用的多个不同位置使用的组件,或者需要在多个不同的应用中使用的组件,那么你绝对不会使用这种方法。但是,如果你正在创建这种全局使用的实用程序,你也不会想在实用程序中使用 Redux、MobX 或任何状态管理工具。

  • 这根本就不是 React 的“行事方式”。或者说……这根本就不符合行业标准
    是啊……我收到过好几次类似的回复。坦白说,每次收到这种回复,我都会对回复者失去一些敬意。抱歉,如果你唯一的借口就是依赖模糊的“方式”,或者援引“行业标准”这个无限延展的妖魔鬼怪,那你就是太懒了。React 刚推出时,并没有提供任何“开箱即用”的状态管理工具。但后来人们开始尝试这个框架,并发现他们需要额外的状态管理工具。于是他们自己开发了这些工具。如果你真的想成为“行业标准”,只需将所有状态变量和所有函数回调都通过 props 传递即可。但是如果你觉得 React 的“基础”实现不能满足你的 100% 需求,那么就不要再对任何未经 Dan Abramov 亲自认可的开箱即用的想法视而不见(和闭上你的头脑)。

那么说什么?

我之所以写这篇文章,是因为我已经在个人项目中使用这种方法很多年了。而且效果非常好。但每次我走出自己的“本地开发圈子”,试图与其他React之外的开发者进行深入的讨论时……我得到的只有教条主义和毫无意义的“行业标准”言论。

这种方法真的很糟糕吗?真的。我知道。但如果它真的是一种“反模式”,如果有人能阐明其“错误”的实证理由,超越“这不是我习惯看到的”这一说法,我将不胜感激。我思想开放。我并不是说这种方法是 React 开发的灵丹妙药。我非常愿意承认它有其自身的局限性。但是,有人能解释一下为什么这种方法完全错误吗?

我真诚地希望您能提供任何反馈,并真诚地期待您的回复 - 即使您的回复带有明显的批评性。

鏂囩珷鏉ユ簮锛�https://dev.to/bytebodger/why-is-this-an-anti-pattern-in-react-427p
PREV
编码测试淘汰了一些最优秀的候选人
NEXT
为亚马逊编写代码是什么样的体验(第一部分)