Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug]: Invalid useEffect warning #8809

Closed
richardgarnier opened this issue Apr 22, 2022 · 39 comments · Fixed by #10394
Closed

[Bug]: Invalid useEffect warning #8809

richardgarnier opened this issue Apr 22, 2022 · 39 comments · Fixed by #10394
Labels

Comments

@richardgarnier
Copy link

What version of React Router are you using?

6.3.0

Steps to Reproduce

Run the following code:

function Child(props) {
  useEffect(() => {
    props.wrappedNavigate('./otherRoute')
  }, []);
  return <></>
}

function Parent() {
  const navigate = useNavigate();
  const wrappedNavigate = (to) => {
    navigate(to);
  }
  
  return <Child wrappedNavigate={wrappedNavigate} />
}

Expected Behavior

Navigate works and performs navigation.

Actual Behavior

You should call navigate() in a React.useEffect(), not when your component is first rendered. is logged into the console.
This is due to the fact that the useEffect of the Child component is executed before the useEffect of the useNavigate, which causes invalid protection on navigation.

@joshribakoff
Copy link

Yeah same issue as #7460

@mastercactapus
Copy link

Came across this while looking into several failing tests. It appears to cause our click events to be lost when running in CI (or locally with CPU throttling) if the click is performed immediately after rendering.

Maybe useLayoutEffect would be a better option here:

React.useEffect(() => {

useEffect runs in the next tick+, not synchronously, so there is a time after everything is rendered and flushed to DOM where a link is rendered and clickable yet this check incorrectly throws away the call to navigate.

Haven't found a reliable workaround yet and editing the library isn't an option for us.

As another suggestion, it might be better to throw an error if it's impossible to continue, rather than silently break things. If it's not a hard requirement to abort navigation, log the warning that unexpected things may happen but continue navigation instead of the following return. Production-mode builds will either work or fail with an error that way.

@42shadow42
Copy link
Contributor

42shadow42 commented Jun 2, 2022

It looks like this was probably introduced based on decisions made here:

If I understand this correctly we can't synchronously navigate in any component render because it causes state updates in other components via this event handler which passes in the state directly.

While I'm no expert here, I think rather than requiring navigation to be triggered after the componentDidMount useEffect indicated in the post above. an alternative solution would be making navigation an asynchronous action. I imagine if navigation requests triggered a setTimeout it would cause navigation to occur after all the useEffects have resolved as demonstrated by this experiment.

By introducing the setTimeout behavior here in the initialization of the navigate function, I think it would eliminate the need for render checking and allow us to safely navigate at any point in the react lifecycle.

Thoughts?

@42shadow42
Copy link
Contributor

Thinking about it further I think useLayoutEffect option proposed by @mastercactapus makes more sense. It doesn't require the use of setTimeout, which is typically discouraged and can sometimes cause unit testing problems. In theory using useLayoutEffect will ensure the ref is updated before any useEffects get called including child components. This resolves the concern from the opening post. Because useLayoutEffect is also executed prior to flushing the updates to the page, it also becomes impossible for test automation to click on elements before the useEffects get executed.

@joshribakoff
Copy link

joshribakoff commented Jun 5, 2022

I think the solution is to remove the overzealous check

navigating isn’t a layout effect (depending on your use case), and normal effects don’t run in the render phase, so the previous few comments I do not agree with

a 1ms setTimeout also works fine and doesn’t cause issues with unit testing so long as you’re not doing something naive like waiting 2ms in your test and expecting the URL to update (you should already be using waitForExpect or waitFor in react-testing-library for effects)

@42shadow42
Copy link
Contributor

@joshribakoff a 1ms timeout would still have a race condition in this case, a 0ms timeout may work though. As for the navigation being a layout effect you are right it's not. Layout effects happen right after the render essentially as part of the render flow. As such they shouldn't be navigating. I believe the render and layout effects happen synchronously and don't allow for any code to be executed between. If you were to attempt navigation in a layout effect in my PR you would get a warning. I believe if you did this during a layout effect you would see similar behavior to the reason to they made the change except you would get 2 warning instead of 1. The second would theoretically explain the root cause of the first which could of been obscure.

@joshribakoff
Copy link

joshribakoff commented Jun 6, 2022

Both a 0ms timeout and 1ms can take longer than 1ms, the 2nd argument passed to setTimeout is just a minimum delay.

The aforementioned testing utilities (waitFor) are capable of waiting until side effects have occurred by repeatedly making observations by running your callback until the assertion passes, so your tests do not need to wait for an (arbitrary) fixed amount of time.

anyway, I think useLayoutEffect is also a valid way to workaround the issue, but ideally the author of this library could actually chime in here or remove the overzealous warning from their library. Then we don’t need any timeout or layout effect workarounds. It seems non ideal for the router to be this coupled to the structure of the tree and the react lifecycle

@ryanflorence
Copy link
Member

I'm not sure why we did that check, looking into it.

@ryanflorence
Copy link
Member

The "correct" React thing to do here is to wrap that function in a useCallback.

function Child(props) {
  useEffect(() => {
    props.wrappedNavigate("./otherRoute");
  }, [props.wrappedNavigate]);
  return <></>;
}

function Parent() {
  const navigate = useNavigate();
  const wrappedNavigate = useCallback(
    (to) => {
      navigate(to);
    },
    [navigate]
  );

  return <Child wrappedNavigate={wrappedNavigate} />;
}

Does the problem persist if you do it this way?

@ryanflorence
Copy link
Member

Also, can somebody make a codesandbox that reproduces this issue? I can't reproduce it.

@joshribakoff
Copy link

joshribakoff commented Jun 10, 2022

No I am already using memoization. I don’t think memoization affects anything about the point in time which the function gets called. It still runs synchronously inside of the child effect. In react children components get their effects processed first, and then the parent. react router is preventing any navigation that occurs prior to the parent effect running. If you were to wrap your call back in a set time out that should be sufficient to delay it until after the parent effect

@ryanflorence
Copy link
Member

We wrap navigate in Remix and don't have this problem. Can you make a reproduction in codesandbox or something?

@joshribakoff-sm
Copy link

joshribakoff-sm commented Jun 10, 2022

https://codesandbox.io/s/dry-wildflower-gn7iv6?file=/src/App.js open console and observe warning from the codepath pertaining to the bug

@joshribakoff-sm
Copy link

joshribakoff-sm commented Jun 10, 2022

Note: the code sandbox / use case may seem contrived, but I would argue its pretty common. In my case, I am trying to call navigate() in something like an onWhatever handler which is defined in the parent.... it's just that the child cannot call it's onWhatever() inside an effect, without the misleading warning (which cancels the navigation). Unlike the warning says, we are not breaking rules of React here (we are not running logic with side effects in the render phase). We're just breaking the heuristic react-router relies on (we're just running in a child effect rather than a parent effect).

@42shadow42
Copy link
Contributor

42shadow42 commented Jun 10, 2022

No I am already using memoization. I don’t think memoization affects anything about the point in time which the function gets called. It still runs synchronously inside of the child effect. In react children components get their effects processed first, and then the parent. react router is preventing any navigation that occurs prior to the parent effect running. If you were to wrap your call back in a set time out that should be sufficient to delay it until after the parent effect

@ryanflorence
Note the following code sandbox:
https://codesandbox.io/s/youthful-monad-ef3nry?file=/src/App.js

@github-actions
Copy link
Contributor

This issue has been automatically closed because we haven't received a response from the original author 🙈. This automation helps keep the issue tracker clean from issues that are unactionable. Please reach out if you have more information for us! 🙂

@joshribakoff
Copy link

😆 @ryanflorence this is not resolved 🤷🏻‍♂️

@richardgarnier
Copy link
Author

richardgarnier commented Jun 27, 2022

I am not sure why this is tagged as needs-response.
Can we consider re-opening this?

EDIT: apparently me commenting fixed it

@rwilliams3088
Copy link

rwilliams3088 commented Jul 24, 2022

Need this fixed. I'm trying to upgrade to react-router v6. In one of my components I would like to immediately update the url params for the page in certain cases (users like the url params to update as they change config on the page so that they can share the links afterwards). Attempting to update the url params (via navigate) in a useEffect causes this warning - and it fails to update the url parameters! I will use the setTimeout hack as a work-around for now, however.

@priyajeet
Copy link

priyajeet commented Aug 5, 2022

I am having an issue with this too. But mostly when using React 18 / react-testing-library v13.3 for unit testing via a memory router. The same unit test worked fine in React 17 / testing library 12.x. Only after an upgrade seeing these issues.

My code is very similar to the one mentioned here #8809 (comment). Only difference is I am calling the navigate function onClick of a button or a link. Navigation fails to happen because of this line if (!activeRef.current) return; because for some reason (useEffects not running in the intended order that react-router expects) at the time of execution activeRef.current is false.

@priyajeet
Copy link

Also want to echo what @mastercactapus mentioned about the warning and silently preventing a navigate. Not sure what was the reasoning behind this as it is hard to decipher what just happened. If it is indeed not desired, it should be an error with better wording. If it is just a warning, navigation should be allowed?

Additionally, if navigates should happen only in effects, should the navigate function now return a promise that folks await on?

@joshribakoff-sm
Copy link

@ryanflorence sorry to ping again but its been several months. We promptly provided the requested reproduction code sandboxes. Can you confirm if you were able to replicate the same on your end? Is react-router still actively maintained? Is there another active maintainer that would be better to tag here?

@42shadow42
Copy link
Contributor

@joshribakoff-sm I was able to reproduce and resolve the issues in theory, but there's a hold up getting approval to sign their CLA, I've been asked for a corporate CLA instead of an individual CLA but one hasn't been provided. I've got the open pull request below although it looks like it has merge conflicts now.

#8929

I'll go back to our open source community and see if I can get approval to do an individual CLA contribution. It's necessary because I put this fix together with company time, because we experienced the same issue.

@joshribakoff-sm
Copy link

Thanks for the update @42shadow42 - I don't see you signed the CLA yet. I took a look at the repo's CLA and it seems to be applicable to any legal entity:

"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Remix.

I'm not sure why your employer would have an issue with the CLA that has been provided.

@priyajeet
Copy link

Thanks for the PR @42shadow42. Removal of the flag helps! But the warning will likely come back, if the child component also uses useLayoutEffect() to trigger the navigate for whatever reason. Because of the hierarchy. So maybe we should additionally mention in the warning message to only navigate in useEffect or async stuff like clicks - not in renders, not in useLayoutEffects?

@42shadow42
Copy link
Contributor

42shadow42 commented Aug 5, 2022

Thanks for the update @42shadow42 - I don't see you signed the CLA yet. I took a look at the repo's CLA and it seems to be applicable to any legal entity:

"You" (or "Your") shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Remix.

I'm not sure why your employer would have an issue with the CLA that has been provided.

Because we were encountering the issue as well and I fixed it on company time, and because of that I interpret them to be the copyright owner? They typically approve these things is my understanding but got hung up the wording that there was a corporate CLA, where remix-run reports having none.

@priyajeet
Copy link

priyajeet commented Aug 8, 2022

BTW @42shadow42 probably should change the git commit message to say fix:... so that release bot knows it is a patch release fix. Unless ofc someone else needs to do that

@RShergold
Copy link

RShergold commented Nov 11, 2022

Is there any progress on this issue? This bug currently breaks all our tests meaning we can not upgrade to React Router 6.

It looks like the PR #8929 has been reviewed but has remained unmerged for months now.

@joshribakoff
Copy link

joshribakoff commented Nov 11, 2022 via email

@machour
Copy link
Contributor

machour commented Nov 11, 2022

@joshribakoff react-router is far from being an abandoned project. Maintainers got busy with other stuff, that's it.
In the meantime, you could apply the changes from the open PR in a fork, or using patch-package until someone from the team have the time to review the pull request.

Patience is key when trying to contribute to OSS.

@joshribakoff-sm
Copy link

I, and others have been forking the project. It feels like in the time it took you to write that comment, you could have just merged the fix. Thanks for the reply.

@machour
Copy link
Contributor

machour commented Nov 11, 2022

@joshribakoff you're very welcome. I'm only handling triage here and don't have karma, nor the required react-router knowledge to even review your PR, let alone merge it. I'll bring this up with the team anyways to have some feedback here. Cheers!

@dtgreene
Copy link

dtgreene commented Dec 1, 2022

Here's another example

@ozkary
Copy link

ozkary commented Dec 5, 2022

When upgrading to React Router V6+, we started to see the navigation warning. We also noticed that the navigation did not take place. To avoid having to make changes all over the codebase, we created an adaptor, which uses a callback to make the calls to the navigate API. This enabled us to support the upgrade without making too many changes.

You should call navigate() in a React.useEffect(), not when your component is first rendered.

The problem when using a useEffect is that it is triggered on first load, so we would need to have a state variable controlling when it should call navigate. For example, a state variable to signal that a click has been made for those use cases.

A created an example here.

https://codesandbox.io/s/react-navigate-useffect-rvnoc7?file=/src/App.js

@wt0m
Copy link

wt0m commented Apr 18, 2023

This bug still exists in April 2023

@brophdawg11
Copy link
Contributor

The parent/child issue describe in here is a legitimate bug due to the order in which effects/layout effects run in parent/children components. Passing a navigate reference to a child component and calling it in an effect should work and is fixed by #10394 - which should be released in 6.11.0.

The warning is correct and will remain because calling navigate in the render cycle on your initial app load is useless because the history listeners/state subscribers have not yet been wired up (since they wire up in effects). So the navigate call will change the URL but React won't be aware of it and you'll be in an invalid state with the new URL but the old content.

@brophdawg11 brophdawg11 added the awaiting release This issue have been fixed and will be released soon label Apr 25, 2023
@brophdawg11 brophdawg11 removed their assignment Apr 25, 2023
@brophdawg11
Copy link
Contributor

This should be available once 6.11 is released

@github-actions
Copy link
Contributor

🤖 Hello there,

We just published version 6.11.0-pre.0 which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

@github-actions
Copy link
Contributor

🤖 Hello there,

We just published version 6.11.0 which involves this issue. If you'd like to take it for a test run please try it out and let us know what you think!

Thanks!

@brophdawg11 brophdawg11 removed the awaiting release This issue have been fixed and will be released soon label Apr 28, 2023
brophdawg11 pushed a commit that referenced this issue Mar 27, 2024
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging a pull request may close this issue.