繁体   English   中英

useEffect 兄弟组件之间的执行顺序

[英]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

问题

是否有一些可靠的来源(例如文档),以便我们可以说useEffectFirstComponent总是先于useEffectChild2?

为什么这是相关的?

如果我们确定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

子元素使用的信息通常存储在父元素的 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 更新解耦:

  1. 有一个axios包装器模块 taht 导出自定义axios实例
  2. 将拦截器放在该实例上
  3. 提供一种从该模块订阅错误事件的方法
  4. 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,您不应该依赖组件的执行顺序来实现任何功能。 原因:

  1. 你想要实现的是反模式,隐式依赖,这让人们感到惊讶。 只是不要这样做。
  2. 毕竟不是很靠谱。 执行顺序保持不变,但不保证连续性。

我将用一些关于 React 组件执行顺序的细节来补充他的回答。

所以对于一个简单的例子:

function A() {
  return (<>
      <B />
      <C>
        <D />
      </C>
    </>
  );
}

确定组件执行顺序的经验法则是根据 JSX 元素创建调用来思考。 在我们的例子中,它将是A(B(), C(D())) ,与 JS function 执行顺序相同,组件的执行(或“渲染”)顺序将是B, D, C, A

但这附带警告:

  1. 如果任何组件退出,例如, DReact.memo编辑的“纯”组件并且它的 props 没有改变,那么顺序将是B, C, A ,保持顺序,但跳过 bailout 组件。

  2. 不太常见的异常,如SuspenseList(实验性)

<SuspenseList>协调其下方最近的<Suspense>节点的“显示顺序”。

哪个原因通过设计影响其子项的执行顺序。

  1. 在并发模式下,由于渲染可以由 React 自行决定中断,因此执行顺序的连续性存在问题。 诸如B, D, B, D, C, A可能会发生。 (也就是说, useEffect似乎不受影响,因为它是在提交阶段调用的 AFAICT)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM