1

In this code:

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> ); } 

The output is:

FirstComponent Child 2 Main App 

Question

Is there some reliable source (e.g. docs) so that we can say that always useEffect of FirstComponent will precede useEffect of Child2?

Why is this relevant?

If we are sure that effect from FirstComponent always runs first, then it could be useful to perform some initialization work there (maybe perform some side effect), which we want to be available to all other useEffects in the app. We can't do this with normal parent/child effects, because you can see that parent effect ("Main App") runs after child effect ("Child 2").

17
  • 2
    I strongly recommend you not count on the order for the purpose you've described, even if it's reliable. Instead, do the necessary setup in the parent component (not in an effect, perhaps in something you do once and store in a ref, or via context, etc.). Crosstalk between sibling components is a very bad idea. Commented Jan 17, 2023 at 8:08
  • @T.J.Crowder Yes, but if the necessary setup contains a side effect, where do I put it then if not in an effect? Commented Jan 17, 2023 at 8:10
  • It depends on the side effect, but as I mentioned, if it's initialization, you can do it once and store the result in a ref. Commented Jan 17, 2023 at 8:11
  • Totally agree w/ TJ Crowder. But just a FYI, the order is just like function calls (because they basically are) A(B(), C(D)). Order would be B, D, C, A. Commented Jan 17, 2023 at 8:15
  • @T.J.Crowder Do you suggest something like this: stackblitz.com/edit/react-v1pn3n?file=src%2FApp.js? Commented Jan 17, 2023 at 8:19

2 Answers 2

2

Answering the question asked: As far as I'm aware, React doesn't guarantee the order of the calls to your component functions for the children, though it would be really surprising if they weren't in first-to-last order between siblings (so, reliably calling FirstComponent at least once before calling Child1 the first time, in that App). But although the calls to createElement will reliably be in that order, the calls to the component functions are done by React when and how it sees fit. It's hard to prove a negative, but I don't think it's documented that they'll be in any particular order.

But:

If we are sure that effect from FirstComponent always runs first, then it could be useful to perform some initialization work there, which we want to be available to all other useEffects in the app. We can't do this with normal parent/child effects, because you can see that parent effect ("Main App") runs after child effect ("Child 2".)

I wouldn't do that even if you find documentation saying the order is guaranteed. Crosstalk between sibling components is not a good idea. It means the components can't be used separately from each other, makes the components harder to test, and is unusual, making it surprising to people maintaining the codebase. You can do it anyway, of course, but as is often the case, lifting state up most likely applies ("state" in the general case, not just component state). Instead, do any one-time initialization you need to do in the parent (App) — not as an effect, but as component state in the parent, or instance state stored in a ref, etc., and pass it to the children via props, context, a Redux store, etc.

In the below, I'll pass things to the children via props, but that's just for the purposes of an example.

State

The usual place to store information used by child elements is in the parent's state. useState supports a callback function that will only be called during initialization. That's where to put this sort of thing unless you have a good reason not to. In the comments, you've suggested you have a good reason not to, but I'd be remiss if I didn't mention it in an answer meant for others in the future, not just for you now.

(This example and the second one below pass the information to the children via props, but again, it could be props, context, a Redux store, etc.)

Example:

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>

Technically, I suppose you could use it for doing something that didn't result in any value at all, but that would be odd semantically and I don't think I'd recommend it.

Non-State

If it's information that can't be stored in state for some reason, you can use a ref, either to store it (although then you might prefer state) or to just store a flag saying "I've done my initialization." One-time initialization of refs is perfectly normal. And if the initialization requires cleanup, you can do that in a useEffect cleanup callback. Here's an example (this example does end up storing something other than a flag on the ref, but it could just be a flag):

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>


Your specific example use case

Re the specific use case you linked (note: the code from the question may not be correct; I'm not trying to correct it here, not least because I don't use axios):

I am using an axios interceptor to handle errors globally, but would like to set the state of my app from the interceptor.

axios.interceptors.response.use( error => { AppState.setError(error) } ) 

(And you've indicated that AppState, whatever it is, is only accessible within App.)

I'm not a fan of modifying the global axios instance, it's another crosstalky thing that affects all code using axios in the page/app, regardless of whether it's your code or code in a library that may use axios in a way where it's not appropriate for your app to show an error state that occurs.

I'd lean toward decoupling the interceptor from the App state update:

  1. Have an axios wrapper module taht exports a custom axios instance
  2. Put the interceptor on that instance
  3. Provide a means of subscribing to error events from that module
  4. Have App subscribe to the error event from that to set its state (and unsubscribe on unmount)

That sounds complicated, but it's only about 30 lines of code even if you don't have a prebuilt event emitter class:

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 {} } }; }); 

Then in App:

import { axios, useErrorListener } from "./axios-wrapper"; function App() { useErrorListener((error) => AppState.setError(error)); // ... } 

In code that needs to use that instance of axios:

import { axios } from "./axios-wrapper"; // ... 

That's a bit barebones (it assumes you'd never have dependencies on the error callback function, for instance), but you get the idea.

Sign up to request clarification or add additional context in comments.

6 Comments

(Unfortunately, while I normally strongly dislike post-and-run, I've been called away unexpectedly so I had to finish up and post and...run. :-| I'll be back in a couple of hours and can address anything that comes up in the comments thing. Sorry about that.)
Thanks I think this is a good answer. Just to make sure, so when you say "just store a flag saying "I've done my initialization."" - "initialization" can include any side effect right? Because docs didn't mention performing side effect along initializing the ref value, but I guess this is assumed because the new VideoPlayer() from the docs could already be doing a side effect for example, no? This is what I meant when I initially shared the docs with you, I knew one time initialization was fine, I didn't know if doing a side effect along with that one time initialization of ref was ok too.
@GiorgiMoniava - Provided you do any necessary cleanup when the component unmounts, yes, AFAIK it's fine to do that kind of thing during the first render if necessary. I'll update the answer to address your example question later (I'm only back for two minutes), but short version: I'd use a ref for it. :-) Although it could be state (the state being the interceptor returned by use), for me a ref makes more sense. So set it up as in the second snippet above (or in a useState callback returning the interceptor) and remove it in an effect cleaup on dismount.
Most examples I've seen don't use return value of interceptors.request.use - if that's what you meant with returning from useState initializer function. But useState would not work in this case IMHO because based on the docs the initializer function of useState should be pure (beta.reactjs.org/reference/react/useState#reference)
@GiorgiMoniava - You need the return value if you're ever going to remove the interceptor. If you never intend to, fine, but in general I don't like writing components that don't clean up properly, even if they are the main app. (And as projects mature I've repeatedly had "the main App component" become a child component instead that does get swapped in and out.) I've added to the end to address your specific use case. Basically, if you want to do it with the global instance, a ref flag and useEffect cleanup does the job, but I'd lean toward decoupling (details above). Happy coding! :-)
|
1

I second to @T.J. Crowder, you should not rely on execution order of components to implement any feature. For reasons:

  1. What you want to achieve is anti-pattern, implicit dependency that surprises ppl. JUST DON'T DO IT.
  2. It's not very reliable after all. The execution order is maintained, but continuity is not guaranteed.

I'll complement his answer with some details on execution order of React components.

So for a simple case of:

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

The rule of thumb to determine component execution order, is to think in terms of JSX element creation call. In our case it'll be A(B(), C(D())), Same as JS function execution order, the execution (or "render") order of components would be B, D, C, A.

But this comes with caveats:

  1. If any component bails out, e.g., D is a React.memo'ed "pure" component and its props doesn't change, then order would be B, C, A, order is maintained, but bailout component is skipped.

  2. Not-so-everyday exception like SuspenseList (experimental)

<SuspenseList> coordinates the “reveal order” of the closest <Suspense> nodes below it.

which of cause affects execution order of its children by design.

  1. In concurrent mode, because rendering can be interrupted at React's discretion, the continuity of execution order is in question. Sth like B, D, B, D, C, A could happen. (That said, useEffect seems unaffected AFAICT cus it's invoked in commit phase)

9 Comments

I will add minor thing if it will affect your answer, see my last edit on the question, I was more interested in the order of useEffects, not the order of element rendering. Even if I know order of element calls in theory their useEffect order maybe different I suppose. I hinted in the title and also in the last section, that I was interested in effect order, but now I see some people could have read it a bit differently.
No it doesn't affect my answer. I intentionally broaden the scope a bit, useEffect order is practically same as component execution order (except caveat #3 as I've pointed out). Still caveat #1 should be a good enough case to talk you out, no?
Anyways, why don't you share the concrete use case that you want to implement with useEffect. If you just want something be done FIRST in <App /> you could simply use useMemo(() => { /* do stuff */ }, []) which will run before any children's useEffect.
Yeah I know it wouldn't largely affect your answer just mentioned it. Now that you mention concurrent mode, actually can't it happen that B rendering gets interrupted, then D and C get commited, and then B gets commited, hence the useEffect order would also change? PS. function which you pass to useMemo wouldn't work because it should be pure based on the docs, so you can't put a side effect there.
Interruption is meant for prioritizing more important work. But what is interrupted must be resumed later, not dropped, so that the committed tree is eventually consistent to the one without interruption. So no, if B doesn’t bailout, then the order is maintained.
|

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.