React:将函数组件作为函数调用

2025-06-08

React:将函数组件作为函数调用

TL;DR

成为组件 ≠ 返回 JSX
<Component />Component()


注意:本文试图解释一个有点高级的概念。

我在 Web 开发中最喜欢的事情之一是,几乎任何问题都可以引发令人难忘的深入探讨,从而揭示出关于非常熟悉的事物的全新内容。

这刚刚发生在我身上,所以现在我对 React 有了更多的了解,并想与你们分享。

一切都始于一个 bug,我们现在将逐步复现它。起点如下:

该应用程序仅包含 2 个组件App& Counter

让我们检查App一下的代码:



const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal(currentTotal => currentTotal + 1);

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

目前还没什么有趣的,对吧?它只是渲染 3Counter秒并跟踪并显示所有计数器的总和。

现在让我们为我们的应用程序添加一个简短的描述:



const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
+ const Description = () => (
+   <p>
+     I like coding counters!
+     Sum of all counters is now {total}
+   </p>
+ );

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
+       <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};


Enter fullscreen mode Exit fullscreen mode

像以前一样完美运行,但现在它有一个闪亮的新描述,很酷!

你可能注意到,我声明了 component ,Description而不是直接在Appreturn 语句中编写 JSX。
这样做的原因可能有很多,比如我想让Appreturn 语句中的 JSX 保持简洁易读,所以我把所有杂乱的 JSX 都移到了Descriptioncomponent 内部。

您可能还注意到我Description 在 inside App声明了。这不是标准方法,但Description需要知道当前状态才能显示总点击次数。
我可以重构它并将其total作为 prop 传递,但我不打算重复使用,Description因为整个应用程序只需要一个!

现在,如果我们还想在中央计数器上方显示一些额外的文本怎么办?让我们尝试添加它:



const App = () => {
  const [total, setTotal] = useState(0);
  const incrementTotal = () => setTotal((currentTotal) => currentTotal + 1);
  const Description = () => (
    <p>
      I like coding counters!
      Sum of all counters is now {total}
    </p>
  );
+
+ const CounterWithWeekday = (props) => {
+   let today;
+   switch (new Date().getDay()) {
+     case 0:
+     case 6:
+       today = "a weekend!";
+       break;
+     case 1:
+       today = "Monday";
+       break;
+     case 2:
+       today = "Tuesday";
+       break;
+     default:
+       today = "some day close to a weekend!";
+       break;
+   }
+
+   return (
+     <div>
+       <Counter {...props} />
+       <br />
+       <span>Today is {today}</span>
+     </div>
+   );
+ };

  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <Counter onClick={incrementTotal} />
+       <CounterWithWeekday onClick={incrementTotal} />
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );
};



Enter fullscreen mode Exit fullscreen mode

太棒了!现在我们确实发现了一个 bug!来看看吧:

请注意,total当您单击中央计数器时,数字是如何递增的,但计数器本身始终保持为 0。

现在,令我惊讶的不是错误本身,而是我偶然发现以下内容可以无缝运行:



  return (
    <div className="App">
      <div>
        <h4>Total Clicks: {total}</h4>
        <Description />
      </div>
      <div className="CountersContainer">
        <Counter onClick={incrementTotal} />
-       <CounterWithWeekday onClick={incrementTotal} />
+       { CounterWithWeekday({ onClick: incrementTotal }) }
        <Counter onClick={incrementTotal} />
      </div>
    </div>
  );


Enter fullscreen mode Exit fullscreen mode

是不是也感到很惊喜?一起来体验吧!

漏洞

出现此错误是因为我们CounterWithWeekday在每次App更新时都会创建一个新的。
这是因为CounterWithWeekday在内部声明了App,这可能被视为反模式

对于这种特殊情况,解决起来很容易。只需将CounterWithWeekday声明移到 之外App,错误就消失了。

Description你可能会想,如果在 内部声明了它,为什么就不会出现同样的问题呢App
其实是有的!只是不太明显,因为 React 重新挂载组件的速度太快了,我们根本无法察觉。而且由于这个组件没有内部状态,所以它不会像 那样丢失CounterWithWeekday

但是为什么直接调用CounterWithWeekday也能解决这个 bug 呢?有没有文档说可以直接把函数式组件当作普通函数来调用?这两个选项有什么区别?函数不应该无论调用方式如何都返回相同的值吗?🤔

我们一步一步来吧。

直接调用

从 React 文档中我们知道组件只是一个普通的 JS 类或函数,最终返回 JSX(大多数时候)。

但是,如果函数式组件只是函数,为什么我们不直接调用它们呢?为什么要使用<Component />语法呢?

事实证明,直接调用在 React 早期版本中是一个相当热门的讨论话题。事实上,这篇文章的作者分享了一个 Babel 插件的链接,它可以帮助你直接调用组件(而无需创建 React 元素)。

我在 React 文档中没有找到任何关于直接调用函数式组件的提及,但是,有一种技术可以证明这种可能性 - render props

经过一些实验,我得出了一个相当奇怪的结论。

组件到底是什么?

返回 JSX、接受道具或在屏幕上渲染某些内容与作为组件无关。

同一个函数可能同时充当组件和普通函数。

作为一个组件,它与拥有自己的生命周期和状态有很大关系。

让我们检查一下<CounterWithWeekday onClick={incrementTotal} />前面的示例在 React dev tools 中的样子:
替代文本

因此,它是一个渲染另一个组件的组件(Counter)。

现在让我们将其更改为{ CounterWithWeekday({ onClick: incrementTotal }) }并再次检查 React devtools:
替代文本

没错!根本没有CounterWithWeekday组件。它根本就不存在。

从 返回的组件Counter和文本CounterWithWeekday现在是 的直接子项App

此外,由于CounterWithWeekday组件不存在,中心Counter不再依赖于其生命周期,因此该错误现在消失了,因此,它的工作方式与其兄弟完全相同Counter

以下是一些我一直困惑的问题的快速解答。希望对大家有所帮助。

为什么CounterWithWeekday组件不再显示在 React dev tools 中?

原因是它不再是一个组件,而只是一个函数调用。

当你做这样的事情时:



const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}


Enter fullscreen mode Exit fullscreen mode

很明显,这个变量text不是组件。
如果它返回 JSX,它就不是组件。
如果它接受一个名为 的参数props,它也不是组件。

一个可以用作组件的函数不一定会用作组件。因此,要成为组件,它需要被用作<Text />其他用途。

与 相同CounterWithWeekday

顺便说一下,组件可以返回纯字符串。

为什么 Counter 现在不会丢失状态?

为了回答这个问题,我们Counter先来回答一下为什么的状态被重置。

以下是具体发生的情况:

  1. CounterWithWeekday在 &内部声明App并用作组件。
  2. 它最初是被渲染的。
  3. 每次更新都会创建App一个新的。CounterWithWeekday
  4. CounterWithWeekday每次App更新都是一个全新的功能,因此,React 无法弄清楚它是同一个组件。
  5. React 每次更新时都会清除CounterWithWeekday的先前输出(包括其子组件),并挂载新的 的CounterWithWeekday输出App。因此,与其他组件不同,CounterWithWeekday永远不会更新,而是始终从头开始挂载。
  6. 由于Counter每次更新时都会重新创建App,因此每次父更新后其状态将始终为 0。

因此,当我们将其CounterWithWeekday作为函数调用时,每次更新时它都会重新声明App,不过,这已经无关紧要了。让我们再次查看 hello world 示例来了解原因:



const HelloWorld = () => {
  const text = () => 'Hello, World';

  return (
    <h2>{text()}</h2>
  );
}


Enter fullscreen mode Exit fullscreen mode

在这种情况下,React 期望更新text时引用保持不变是没有意义的,对吗?HelloWorld

事实上,React甚至无法检查text引用是什么。它根本不知道引用的存在。如果我们像这样text内联,React 根本察觉不到其中的区别:text



const HelloWorld = () => {
- const text = () => 'Hello, World';
-
  return (
-   <h2>{text()}</h2>
+   <h2>Hello, World</h2>
  );
}


Enter fullscreen mode Exit fullscreen mode

因此,通过使用 ,<Component />我们使组件对 React 可见。但是,由于text在我们的示例中只是直接调用,React 永远不会知道它的存在。
在这种情况下,React 只会比较 JSX(在本例中为文本)。直到 返回的内容text相同为止,不会重新渲染任何内容。

这正是 发生的事情CounterWithWeekday。如果我们不像 那样使用它<CounterWithWeekday />,它就永远不会暴露给 React。

这样,React 只会比较函数的输出,而不会比较函数本身(如果我们将其用作组件,则会比较)。
由于CounterWithWeekday的输出正常,因此不会重新挂载任何内容。

结论

  • 返回 JSX 的函数可能不是组件,这取决于它的使用方式。

  • 要成为返回 JSX 的组件函数,应该使用 as<Component />而不是 as Component()

  • 当使用功能组件时,<Component />它将具有生命周期并且可以具有状态。

  • 直接调用函数时,Component()它会直接运行并(可能)返回一些值。没有生命周期,没有钩子,没有任何 React 的魔力。这非常类似于将 JSX 赋值给变量,但灵活性更高(可以使用 if 语句、switch、throw 等)。

  • 在非组件中使用状态是危险的

  • 使用返回非组件 JSX 的函数在未来可能会被正式视为反模式。虽然存在一些边缘情况(例如 render props),但通常情况下,你几乎总是希望将这些函数重构为组件,因为这是推荐的方式。

  • 如果您必须声明一个在功能组件内返回 JSX 的函数(例如,由于紧密耦合的逻辑),那么直接调用它{component()}可能是比使用它更好的选择<Component />

  • 将简单转换<Component />{Component()}可能对于调试目的非常方便。

鏂囩珷鏉ユ簮锛�https://dev.to/igor_bykov/react-calling-functions-components-as-functions-1d3l
PREV
在 Ubuntu 22.04 中运行/安装虚幻引擎 5
NEXT
在您的 Vue 和 Nuxt 应用程序中最小化、生成 WebP 和延迟加载图像