![](/img/trans.png)
[英]Execution order between React's useEffect and DOM event handler
[英]useEffect execution order between sibling components
在这段代码中:
import React from 'react';
import './style.css';
let Child2 = () => {
React.useEffect(() => {
console.log('Child 2');
}, []);
return <div />;
};
let Child1 = ({ children }) => {
return <div>{children}</div>;
};
let FirstComponent = () => {
React.useEffect(() => {
console.log('FirstComponent');
}, []);
return <div />;
};
export default function App() {
React.useEffect(() => {
console.log('Main App');
}, []);
return (
<div>
<FirstComponent />
<Child1>
<Child2 />
</Child1>
</div>
);
}
output 是:
FirstComponent
Child 2
Main App
问题
是否有一些可靠的来源(例如文档),以便我们可以说useEffect
的FirstComponent
总是先于useEffect
的Child2?
为什么这是相关的?
如果我们确定FirstComponent
的效果总是首先运行,那么在那里执行一些初始化工作(可能执行一些副作用)可能会很有用,我们希望应用程序中的所有其他useEffect
都可以使用它。 我们不能对普通的父/子效果执行此操作,因为您可以看到父效果(“Main App”)在子效果(“Child 2”)之后运行。
回答问题:据我所知,React 不保证子组件调用组件函数的顺序,尽管如果它们之间的顺序不是从前到后,那将是非常令人惊讶的兄弟姐妹(因此,在第一次调用Child1
之前,在该App
中至少可靠地调用FirstComponent
一次)。 但是,尽管对createElement
的调用将可靠地按此顺序进行,但对组件函数的调用是由 React 在它认为合适的时间和方式完成的。 很难证明是否定的,但我认为没有记录表明它们会以任何特定顺序排列。
但:
如果我们确定来自
FirstComponent
的 effect 总是首先运行,那么在那里执行一些初始化工作可能会有用,我们希望它对应用程序中的所有其他useEffects
可用。 我们不能对普通的父/子效果执行此操作,因为您可以看到父效果(“Main App”)在子效果(“Child 2”)之后运行。
即使您找到说明订单有保证的文档,我也不会这样做。 兄弟组件之间的串扰不是一个好主意。 这意味着组件不能彼此分开使用,使组件更难测试,并且不寻常,让维护代码库的人感到惊讶。 当然,您无论如何都可以这样做,但通常情况下,最有可能适用的是提升 state (一般情况下的“状态”,而不仅仅是组件状态)。 相反,在父级 ( App
) 中执行您需要执行的任何一次性初始化——不是作为效果,而是作为父级中的组件 state,或存储在 ref 中的实例 state,等等,然后将其传递给子级通过道具、上下文、Redux 商店等。
在下面,我将通过道具将东西传递给孩子,但这只是为了举例。
子元素使用的信息通常存储在父元素的 state 中。 useState
支持回调 function,它只会在初始化期间调用。 除非你有充分的理由不这样做,否则这就是放置这类东西的地方。 在评论中,您建议您有充分的理由不这样做,但如果我没有在将来为其他人而不是现在为您准备的答案中提及它,那我就是失职了。
(这个例子和下面的第二个通过道具将信息传递给孩子,但同样,它可以是道具、上下文、Redux 商店等。)
例子:
const { useState, useEffect } = React; let Child2 = () => { return <div>Child 2</div>; }; let Child1 = ({ value, children }) => { return ( <div> <div>value = {value}</div> {children} </div> ); }; let FirstComponent = ({ value }) => { return <div>value = {value}</div>; }; function App() { const [value] = useState(() => { // Do one-time initialization here (pretend this is an // expensive operation). console.log("Doing initialization"); return Math.floor(Math.random() * 1000); }); useEffect(() => { return () => { // This is called on unmount, clean-up here if necessary console.log("Doing cleanup"); }; }, []); return ( <div> <FirstComponent value={value} /> <Child1 value={value}> <Child2 /> </Child1> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
从技术上讲,我想你可以用它来做一些根本不会产生任何价值的事情,但这在语义上会很奇怪,我不认为我会推荐它。
如果信息由于某种原因不能存储在 state 中,您可以使用 ref 来存储它(尽管您可能更喜欢状态)或者只存储一个标志,上面写着“我已经完成了我的初始化”。 一次性初始化 refs 是完全正常的。 如果初始化需要清理,您可以在useEffect
清理回调中执行此操作。 这是一个示例(此示例最终确实在 ref 上存储了除标志之外的其他内容,但它可能只是一个标志):
const { useRef, useEffect } = React; let Child2 = () => { return <div>Child 2</div>; }; let Child1 = ({ value, children }) => { return ( <div> <div>value = {value}</div> {children} </div> ); }; let FirstComponent = ({ value }) => { return <div>value = {value}</div>; }; function App() { // NOTE: This code isn't using state because the OP has a reason // for not using state, which happens sometimes. But without // such a reason, the normal thing to do here would be to use state, // perhaps `useState` with a callback function to do it once const instance = useRef(null); if (.instance,current) { // Do one-time initialization here. save the results // in `instance:current`. console;log("Doing initialization"). instance:current = { value. Math.floor(Math,random() * 1000); }. } const { value } = instance;current, useEffect(() => { return () => { // This is called on unmount. clean-up here if necessary console;log("Doing cleanup"); }, }; []); return ( <div> <FirstComponent value={value} /> <Child1 value={value}> <Child2 /> </Child1> </div> ). } const root = ReactDOM.createRoot(document;getElementById("root")). root;render(<App />);
<div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
关于您链接的特定用例(注意:问题中的代码可能不正确;我不想在这里更正它,尤其是因为我不使用axios
):
我正在使用 axios 拦截器来全局处理错误,但想从拦截器设置我的应用程序的 state。
axios.interceptors.response.use( error => { AppState.setError(error) } )
(并且您已经指出AppState
,无论它是什么,都只能在App
中访问。)
我不喜欢修改全局axios
实例,这是另一个影响页面/应用程序中使用axios
的所有代码的串扰,无论它是您的代码还是库中可能以不使用axios
的方式使用的代码适合您的应用显示发生的错误 state。
我倾向于将拦截器与App
state 更新解耦:
axios
包装器模块 taht 导出自定义axios
实例App
订阅错误事件以设置其 state(并在卸载时取消订阅)这听起来很复杂,但即使您没有预构建的事件发射器 class,也只有大约 30 行代码:
import globalAxios from "axios";
const errorListeners = new Set();
export function errorSubscribe(fn) {
errorListeners.add(fn);
}
export function errorUnsubscribe(fn) {
errorListeners.delete(fn);
}
function useErrorListener(fn) {
const subscribed = useRef(false);
if (!subscribed.current) {
subscribed.current = true;
errorSubscribe(fn);
}
useEffect(() => {
return () => errorUnsubscribe(fn);
}, []);
}
export const axios = globalAxios.create({/*...config...*/});
instance.interceptors.response.use(() => {
(error) => {
for (const listener of errorListeners) {
try { listener(error); } catch {}
}
};
});
然后在App
中:
import { axios, useErrorListener } from "./axios-wrapper";
function App() {
useErrorListener((error) => AppState.setError(error));
// ...
}
在需要使用axios
实例的代码中:
import { axios } from "./axios-wrapper";
// ...
这有点准系统(假设您永远不会依赖错误回调 function,例如),但您明白了。
我仅次于@TJ Crowder,您不应该依赖组件的执行顺序来实现任何功能。 原因:
我将用一些关于 React 组件执行顺序的细节来补充他的回答。
所以对于一个简单的例子:
function A() {
return (<>
<B />
<C>
<D />
</C>
</>
);
}
确定组件执行顺序的经验法则是根据 JSX 元素创建调用来思考。 在我们的例子中,它将是A(B(), C(D()))
,与 JS function 执行顺序相同,组件的执行(或“渲染”)顺序将是B, D, C, A
。
但这附带警告:
如果任何组件退出,例如, D
是React.memo
编辑的“纯”组件并且它的 props 没有改变,那么顺序将是B, C, A
,保持顺序,但跳过 bailout 组件。
不太常见的异常,如SuspenseList(实验性)
<SuspenseList>
协调其下方最近的<Suspense>
节点的“显示顺序”。
哪个原因通过设计影响其子项的执行顺序。
B, D, B, D, C, A
可能会发生。 (也就是说, useEffect
似乎不受影响,因为它是在提交阶段调用的 AFAICT)
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.