facebook / react

The library for web and native user interfaces.
https://react.dev
MIT License
229.27k stars 46.95k forks source link

useEffect firing in children before parent #15281

Closed mpgon closed 5 years ago

mpgon commented 5 years ago

Do you want to request a feature or report a bug? Feature (I believe) What is the current behavior? Right now, the effects in useEffect fire children-first. I know this makes sense, since the behaviour of the first render has a close correlation to cDm. However, I cannot seem to find a way to fire events parent-first. Before, I could do it with cWm, and after that was deprecated, in the constructor. How can I accomplish that with hooks? CodeSandbox example: https://codesandbox.io/s/035lqnozzl?fontsize=14 What is the use case? Imagine I want to post to an external server when two components were first rendered, to measure a sort of meaningful paint.

- Component A -> post("first render"", component: A)
----
--------
------------ Component B -> post("first render", component: B)

How could I accomplish this with hooks?

gaearon commented 5 years ago

Why would the order matter in that case? First paint would happen for both at the same time.

mpgon commented 5 years ago

In this simple example yes, but imagine wanting to measure this in an expensive tree, where you have intermediate components with expensive fetches, etc. For example, imagine you want to measure the difference between when they see the navbar and they see a post detail.

gaearon commented 5 years ago

But that's not how React works. It won't somehow paint the child separate from the parent. Maybe you can add an example where it matters? I don't understand the pattern you're referring to.

gaearon commented 5 years ago

If you mean that the child is mounted later — why does the order matter then?

mpgon commented 5 years ago

I'm thinking of a situation where the child first render happens after the parent first render, but the effect of the child is fired first. To clarify: t=0

<Parent useEffect("effect parent", [])> 
    <Spinner />
</Parent>

t=1

<Parent>
    <Child useEffect("effect child", []) />
</Parent>

and the "effect child" can be fired before "effect parent"

mpgon commented 5 years ago

This one illustrates it better. https://codesandbox.io/s/j3mr80r9m9?fontsize=14 Although admittedly it doesn't seem to reproduce what I'm experiencing in my app. So the question is, is it impossible, in this codesandox example, for the CHILD first render to ever be flushed before the PARENT first render?

gaearon commented 5 years ago

A child's first render can't be flushed before parent's first render by definition — a child is a part of the parent.

mpgon commented 5 years ago

That's what I was trying to find out. Then the problem must lie elsewhere. Thank you for your time!

dy commented 5 years ago

Can we please elaborate the answer? Facing the same situation https://codesandbox.io/s/0qx6lq4lrn?fontsize=14.

Basically I hoped to run "startup" code, setting up API, managing saved session, redirecting to sign-in if user is not authenticated, connecting store to network events etc., basically setting up App state. First expectation for that is to put useEffect at the root level, ie. in App:

import React, { useEffect } from "react";
import ReactDOM from "react-dom";
import { navigate } from "hookrouter";

import "./styles.css";

function App() {
  let auth = useStore('auth')
  useEffect(() => {
    console.log("startup");
    if (!auth.tokens) navigate('/sign-in')
    // ...restore saved session, redirects etc.
  }, []);

  return <Content />;
}

function Content() {
  useEffect(() => {
    console.log("init data");
    // ...fetch content etc.
  }, []);

  return <></>;
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

But the children effects run before the App. What would be the right way to organize startup?

@gaearon @mpgon

dy commented 5 years ago

Ok, what I needed is useAsync:


  // startup
  let [complete, error, loading] = useAsync(async () => {
    // auth
    if (auth) {
      setToken(auth)
    }

    if (isSignedIn === false) {
      navigate('/sign-in')
    }
    if (resetPasswordUserLogin) {
      navigate('/password/reset')
    }

    return true
  }, [])
nikparo commented 3 years ago

As far as I can tell child effects run in order, first in first out, all else being equal. I created a tiny component to capitalise on this behaviour and run "parent" effects first:

export function Effect({ effect }) {
  useEffect(() => effect?.(), [effect]);
  return null;
}

See https://gist.github.com/nikparo/33544fe0228dd5aa6f0de8d03e96c378 for more details.

abetss commented 3 years ago

I'm facing an issue with this now. I'm abstracting the updating of page title in a component that wraps children with Route component from react-router-dom. When I create nested routes with that, the title of the children components updates before its parent, therefore the page title is overridden by its parent page title.

This is an arbitrary example of that

import { useEffect } from 'react';
import { Redirect, Route } from 'react-router-dom';

export function CustomRoute({
  children,
  title,
  ...props
}) {
  useEffect(() => {
    if (title) {
      document.title = title;
    }
  }, [title]);

  return <Route {...props}>{children}</Route>;
}

function AppRouter() {
  return (
    <Switch>
      <CustomRoute title="Profile" path="/profile">
        <ParnetComponent />
      </CustomRoute>
    </Switch>
  )
}

function Profile() {
  return (
    <Switch>
      <CustomRoute title="Create - Profile" path="/profile/create">
        <CreateProfile />
      </CustomRoute>
    </Switch>
  )
}

function CreateProfile() {
  return (
    <Switch>
      <CustomRoute path="/profile/create/personal-info" title="Personal Info - Create - Profile">
        <PersonalInfo />
      </CustomRoute>
      <CustomRoute path="/profile/create/history" title="Historty Info - Create - Profile">
        <History />
      </CustomRoute>
      <CustomRoute path="/profile/create/completed" title="Completed - Create - Profile">
        <Completed />
      </CustomRoute>
      <Route
            exact
            path="/"
            render={() => (
              <Redirect
                to={{
                  pathname: '/profile/create/personal-info',
                }}
              />
            )}
          />
    </Switch>
  )
}

For this use case the useEffect order matters to me.

nikparo commented 3 years ago

@abetss You could use my idea above to get around that. In short, you would end up with something like this:

function DocTitle({ title }) {
  useEffect(() => {
    if (title) {
      document.title = title;
    }
  }, [title]);

  return null;
}

export function CustomRoute({
  children,
  title,
  ...props
}) {
  // Child effects are run in order. Therefore the title is updated before any other effects are called.
  return (
    <Route {...props}>
      <DocTitle title={title} />
      {children}
    </Route>
  );
}

Edit: Moved <DocTitle /> to be within <Route />.

vezaynk commented 3 years ago

Hi @abetss,

Your probably already found a satisfactory solution, but here is another: https://gist.github.com/knyzorg/a4395fcc4e8d53d5be4dff4ef5228379

It's more intricate, but it comes with unit tests.

mytoandeptrai commented 1 year ago

Well, i am currently facing this issue right now. I am abstracting that when I reload a page then a page containing child component will request an API containing value from the parent component. But the child component always fired first before the parent component. So that is my solution to fix it, you just use useLayoutEffect on the parent component instead of using useEffect. That's my example code, hope it will help you and someone else.

import React, { useEffect, useLayoutEffect } from "react";
import ReactDOM from "react-dom";

function Parent({ children }) {
  useLayoutEffect(() => {
    console.log("Parent first render");
  });

  return (
    <div>
      <h2>parent</h2>
      {children}
    </div>
  );
}

function Child() {
  useEffect(() => {
    console.log("Child will render after");
  });

  return <h3>child</h3>;
}

function App() {
  return (
    <Parent>
      <Child />
    </Parent>
  );
}
}

364630587_303322415435368_4757988254336755015_n