出于好奇,我想直接调用一个React函数组件;就像一个普通函数一样。然而,React抱怨道:
Invalid hook call. Hooks can only be called inside of the body of a function component.
React * 如何知道 * 这一点?下面是示例代码:
const T = () => {
useEffect(() => console.log("effect"));
return (
<>
<div> hello </div>
</>
);
};
我像这样提取useEffect
:
const T = (func = useEffect) => () => {
func(() => console.log("effect"));
return (
<>
<div> hello </div>
</>
);
};
然后通过以下方式重新触发:T()()
不起作用。但是T(f=>f())()
起作用了。
这对 every 钩子是真的吗,还是只对React提供的钩子是特殊的?创建一个任意的定制钩子(不依赖于现有的React钩子)也会导致相同的错误?React在内部发生了什么来确定这一点?* 为什么 * 尝试并执行此操作一定是错误?根据我的理解,这些只是返回React元素的JS * 函数 *(React.createElement
)它们本身只是JS对象,不是吗?我错过了什么?除了提取每个钩子依赖并传递一个存根实现之外,有没有办法强制执行?
1条答案
按热度按时间baubqpgj1#
你可以在React源代码中看到它的设置位置,在挂载的组件之外,标准钩子(
useEffect
,useState
等)被初始化为throwInvalidHookError
,这是显而易见的。在挂载的函数组件中,
useEffect
被设置为mountEffect
函数,该函数执行预期的效果-运行内容。自定义钩子不会被特别对待(尽管the standard linting rules会报告任何滥用),但是一个不调用任何标准钩子的自定义钩子--无论是直接还是间接--根本不需要成为钩子。如果它调用了标准钩子之一,那么很明显,
Invalid hook call
异常将被抛出。这肯定是一个错误,因为标准钩子在React机制中有特殊处理,"rules of hooks"导致每个组件中钩子的顺序一致;它们被强制执行,以便React可以维护与钩子调用相关联的内部状态。
这是React作者的设计选择。设置了一些限制,它导致了实现的优化。
根据我的理解,这些只是返回React元素的JS函数
是的,组件只是返回React节点树表示的JS函数,然后React将此节点树转换为HTML节点,并带有相关的事件处理程序等。
除了提取每个钩子依赖项并传入一个存根实现之外,没有其他方法可以做到这一点吗?
不完全清楚您在这里要做什么,但简短的回答似乎是"正确的,不重新构造代码是没有办法的"。您可以使用常规函数调用常规函数,或者使用钩子调用钩子(和常规函数)。这并不神奇--你可以把你自己的函数变成一个钩子,只要把它的名字以
use
开头,那么它可以调用其他钩子,只要它遵循钩子的规则。编辑
我只想像这样调用组件T()。
现在还不清楚你希望通过这样做来解决什么问题。组件的返回值是一个反映组件树的数据结构。你可以通过编写一个无钩子的组件来了解:
Sandbox
因此,这是一个熟悉的树结构,其中包含一些额外的插槽用于React内务处理数据。
我们可以通过从一个已挂载的组件调用
HelloWorld()
来进一步探索:Sandbox
这将呈现一个空的
<div></div>
,但是HelloWorld()
的返回值包含一些新的内部元素:FiberNode
值暗示了React在幕后所做的事情-Fiber is the algorithm that does what React does、FiberNode
is an internal data structure it uses to do that。如果我们给
HelloWorld()
添加一个钩子调用会发生什么?Sandbox
当然,如果我们直接调用它,那么我们会得到熟悉的
Invalid hook call
错误,但是如果我们从一个挂载的组件调用它......我们会在控制台输出上看到Howdy!
。这是因为,正如你提到的,这些都是普通的老式Javascript函数。从
HelloWorld()
和<App/>
调用useEffect()
与直接从<App/>
调用useEffect()
的工作相同。然而,这与从<HelloWorld/>
调用useEffect()
是 * 不 * 相同的。让我们试试别的方法,如果我们创建一个
<HelloWorld/>
元素会发生什么?Sandbox
我们不再得到
Howdy!
,返回值也不同:<HelloWorld/>
只是调用createElement(HelloWorld)
的语法糖,只有在呈现该元素时,函数才会被调用。通过比较返回值,我们还可以看到子节点从外部不再可见,树结构只是一个
HelloWorld
节点,而不是嵌套的div
/span
/text结构。让我们再试一次,如果我们只是偶尔调用
HelloWorld
呢?Sandbox
单击
<div>
将更新count
,每次调用不同的对象。在第一次加载时,我们看到
Hello World!
呈现在浏览器中,控制台显示:HelloWorld()
被调用了两次(这为我们提供了两个问候语)-一次是在呈现它时,另一次是在直接调用时。单击以获得第二个渲染,我们将看到:
我们不再呈现
<HelloWorld/>
,所以我们只看到来自直接调用HelloWorld()
的问候语。单击以获取第三个渲染...
啊哈!我们违反了钩子的规则,React抱怨了这一点。前两次我们渲染
<App/>
时,调用了useEffect()
钩子,但第三次没有。钩子不能有条件地调用。<HelloWorld/>
时,它没有抱怨。这是同一个函数调用同一个钩子!这太离谱了,太不公平了,等等。* 每个组件示例都有自己的分类账来跟踪钩子。当我们直接调用HelloWorld()
时,useEffect()
会在App
示例中被跟踪。当我们渲染<HelloWorld/>
时,useEffect()
在HelloWorld
示例中被跟踪。如果我们渲染<HelloWorld/>
多次,每个示例跟踪它自己的useEffect()
调用。抱歉,在React函数组件和效果的幕后进行了冗长但不特别全面的窥视。我希望它在某种程度上解决了你的开放式问题:“为什么尝试这样做一定是一个错误?”/“我错过了什么?”
最简单的答案是“当您将
<Foo/>
放入组件树时,您就是not calling it directly“。根据你最初的假设:“出于好奇,我想直接调用一个React函数组件”,你发现了发生了什么。React抱怨!如果你有一匹驮马,你去除了氧气和重力,它也不太好用。