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>
);
};
目前还没什么有趣的,对吧?它只是渲染 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>
);
};
像以前一样完美运行,但现在它有一个闪亮的新描述,很酷!
你可能注意到,我声明了 component ,Description
而不是直接在App
return 语句中编写 JSX。
这样做的原因可能有很多,比如我想让App
return 语句中的 JSX 保持简洁易读,所以我把所有杂乱的 JSX 都移到了Description
component 内部。
您可能还注意到我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>
);
};
太棒了!现在我们确实发现了一个 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>
);
是不是也感到很惊喜?一起来体验吧!
漏洞
出现此错误是因为我们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>
);
}
很明显,这个变量text
不是组件。
如果它返回 JSX,它就不是组件。
如果它接受一个名为 的参数props
,它也不是组件。
一个可以用作组件的函数不一定会用作组件。因此,要成为组件,它需要被用作<Text />
其他用途。
与 相同CounterWithWeekday
。
顺便说一下,组件可以返回纯字符串。
为什么 Counter 现在不会丢失状态?
为了回答这个问题,我们Counter
先来回答一下为什么的状态被重置。
以下是具体发生的情况:
CounterWithWeekday
在 &内部声明App
并用作组件。- 它最初是被渲染的。
- 每次更新都会创建
App
一个新的。CounterWithWeekday
CounterWithWeekday
每次App
更新都是一个全新的功能,因此,React 无法弄清楚它是同一个组件。- React 每次更新时都会清除
CounterWithWeekday
的先前输出(包括其子组件),并挂载新的 的CounterWithWeekday
输出App
。因此,与其他组件不同,CounterWithWeekday
永远不会更新,而是始终从头开始挂载。 - 由于
Counter
每次更新时都会重新创建App
,因此每次父更新后其状态将始终为 0。
因此,当我们将其CounterWithWeekday
作为函数调用时,每次更新时它都会重新声明App
,不过,这已经无关紧要了。让我们再次查看 hello world 示例来了解原因:
const HelloWorld = () => {
const text = () => 'Hello, World';
return (
<h2>{text()}</h2>
);
}
在这种情况下,React 期望更新text
时引用保持不变是没有意义的,对吗?HelloWorld
事实上,React甚至无法检查text
引用是什么。它根本不知道引用的存在。如果我们像这样text
内联,React 根本察觉不到其中的区别:text
const HelloWorld = () => {
- const text = () => 'Hello, World';
-
return (
- <h2>{text()}</h2>
+ <h2>Hello, World</h2>
);
}
因此,通过使用 ,<Component />
我们使组件对 React 可见。但是,由于text
在我们的示例中只是直接调用,React 永远不会知道它的存在。
在这种情况下,React 只会比较 JSX(在本例中为文本)。直到 返回的内容text
相同为止,不会重新渲染任何内容。
这正是 发生的事情CounterWithWeekday
。如果我们不像 那样使用它<CounterWithWeekday />
,它就永远不会暴露给 React。
这样,React 只会比较函数的输出,而不会比较函数本身(如果我们将其用作组件,则会比较)。
由于CounterWithWeekday
的输出正常,因此不会重新挂载任何内容。
结论
-
返回 JSX 的函数可能不是组件,这取决于它的使用方式。
-
要成为返回 JSX 的组件函数,应该使用 as
<Component />
而不是 asComponent()
。 -
当使用功能组件时,
<Component />
它将具有生命周期并且可以具有状态。 -
直接调用函数时,
Component()
它会直接运行并(可能)返回一些值。没有生命周期,没有钩子,没有任何 React 的魔力。这非常类似于将 JSX 赋值给变量,但灵活性更高(可以使用 if 语句、switch、throw 等)。 -
使用返回非组件 JSX 的函数在未来可能会被正式视为反模式。虽然存在一些边缘情况(例如 render props),但通常情况下,你几乎总是希望将这些函数重构为组件,因为这是推荐的方式。
-
如果您必须声明一个在功能组件内返回 JSX 的函数(例如,由于紧密耦合的逻辑),那么直接调用它
{component()}
可能是比使用它更好的选择<Component />
。 -
将简单转换
<Component />
为{Component()}
可能对于调试目的非常方便。