React Router useNavigate with a useEffect hook - proper way to use?
Asked Answered
G

6

7

I'm new to React and trying to make a loading/greeting page that navigates on to the next after a few seconds of being shown. In React Router v6, we have the useNavigate() hook to allow you to control the navigation, and I am using this to successfully call the navigate function by setting a timeout in a useEffect() hook. However, the compiler is complaining that I have a missing dependency. I only want it to run once though, not whenever the navigate changes. What is the best way to do this?

Thanks!

import { useEffect } from "react";
import { useNavigate } from "react-router-dom";

function Greeting(props) {
  const navigate = useNavigate();
  useEffect(() => {
    setTimeout(() => navigate(props.nextPage), 3000);
  }, []);

  return (
    <div className="Greeting">
      <div>Hello World!</div>
    </div>
  );
}

export default Greeting;

Line 9:6: React Hook useEffect has a missing dependency: 'navigate'. Either include it or remove the dependency array react-hooks/exhaustive-deps

Glyptics answered 14/9, 2022 at 17:32 Comment(0)
B
7

The useEffect hook is missing dependencies, both the navigate function and props.nextPage that are referenced in the callback. The linter warning is informing you to add them to the dependency array. The navigate function is a stable reference so it's completely safe to always be included as a dependency, so this leaves the nextPage prop value that should be included so it's re-enclosed in the timeout callback. Don't forget to return a cleanup function in the case that the component unmounts prior to the timeout expiring on its own.

useEffect(() => {
  const timerId = setTimeout(() => navigate(props.nextPage), 3000);
  return () => clearTimeout(timerId);
}, [navigate, props.nextPage]);

As a general rule you should follow all guidance from the linter. The react-hooks/exhaustive-deps rule is there to help you write better code. Don't disable the rule for that line unless you are absolutely sure and are aware of what future consequences may arise if/when you ever update the callback logic. Disabling the linter rule will potentially mask future issues if the implementation changes and you do actually want to run the effect more often. My rule for this occasion is to add a comment to my future self and other team members to be aware they should check the dependency array.

Example:

useEffect(() => {
  const timerId = setTimeout(() => navigate(props.nextPage), 3000);
 
  return () => clearTimeout(timerId);
 
  // NOTE: Run effect once on component mount. Re-check dependencies
  // if effect logic is updated.
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Bubal answered 14/9, 2022 at 17:42 Comment(4)
This answer is misleading. The linter is not all-knowing and you should not follow it's suggestions every time. See my answer for the explanationAnalisaanalise
@Analisaanalise What exactly is misleading? The linter warns about missing dependencies, and you are free to decide what to ignore or not.Bubal
You said that "useEffect hook is missing dependencies". But there are valid cases where you do NOT want navigate as a dependency. You can't say that it should be added just because in most cases that is true. In case of this user's question I'd argue that it wont matter if dependencies are added or not.Analisaanalise
@Analisaanalise In any case navigate is an external (to the useEffect hook) dependency, and you'll get the linter warning if you omit navigate. Like my answer states though, you can disable the rule for that line to signify to the linter "No really, I know what I'm doing here." You're not wrong that the linter isn't very smart, but it is really good at spotting missing external dependencies. It can't understand what the dependencies mean, and it can't read your mind what you want the effect to be and when you really want it to run.Bubal
A
14

However, the compiler is complaining that I have a missing dependency

That is not a compiler error, it's an eslint warning.

ESLint is not very smart and does not know if something should be added as a dependency or not, so it defaults to warning you about potential problems and you are then free to disable this warning if you know that it does not apply to you.

Should you add navigate to the list of dependencies when using it in useEffect ?

It depends.

The useNavigate() hook depends on useLocation().pathname + a few other things.

So we can change the question to this:

Should your useEffect hook run again if the path changes?

Reframing the question like this should make the answer more obvious for new devs.

Important note:
You will need to add navigate as a dependency if you are navigating using relative paths.
This is because navigate uses useLocation().pathname to resolve relative paths.
In general I would advise against using relative paths when possible.

The accepted answer here says you should always add navigate to the list of dependencies, but this can easily lead to problems that are hard to debug if you don't know that navigate can change.

In most cases your component will only exist on one path, so it wont matter what you do.

The other people answering here apparently don't have much experience with react-router, so they likely never encountered the case where the choice made a difference.

Anyway, here are your choices:

  • Re-run the hook if path changes or if props.nextPage changes
const navigate = useNavigate();
useEffect(() => {
  const timeout = setTimeout(() => navigate(props.nextPage), 3000);
  return () => clearTimeout(timeout);
}, [navigate, props.nextPage]);
  • Re-run the hook only if props.nextPage changes
const navigate = useNavigate();
useEffect(() => {
  const timeout = setTimeout(() => navigate(props.nextPage), 3000);
  return () => clearTimeout(timeout);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.nextPage]);
  • Don't re-run the hook in any case (only run once)
const navigate = useNavigate();
useEffect(() => {
  const timeout = setTimeout(() => navigate(props.nextPage), 3000);
  return () => clearTimeout(timeout);
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Note about useEffect callbacks:

  • In order to reset the timeout when useEffect is re-run, I've added a callback that runs clearTimeout().
  • You also need to cancel the timeout if the user decides not to wait and opens another page. For example, if you have a "Next" button then you will also need to cancel the timeout.
  • Having this callback is also necessary if you have react strict mode enabled.

trying to make a loading/greeting page that navigates on to the next after a few seconds of being shown

I'm fairly sure you're not going to change the path or the nextPage prop during those few seconds, so in your case it won't make a difference whether you add the dependencies or not.

The only reason why I can suggest adding variables even when they are not necessary is because disabling eslint warnings will make it more likely that you forget to add variables that do change.

Analisaanalise answered 2/2, 2023 at 22:40 Comment(0)
B
7

The useEffect hook is missing dependencies, both the navigate function and props.nextPage that are referenced in the callback. The linter warning is informing you to add them to the dependency array. The navigate function is a stable reference so it's completely safe to always be included as a dependency, so this leaves the nextPage prop value that should be included so it's re-enclosed in the timeout callback. Don't forget to return a cleanup function in the case that the component unmounts prior to the timeout expiring on its own.

useEffect(() => {
  const timerId = setTimeout(() => navigate(props.nextPage), 3000);
  return () => clearTimeout(timerId);
}, [navigate, props.nextPage]);

As a general rule you should follow all guidance from the linter. The react-hooks/exhaustive-deps rule is there to help you write better code. Don't disable the rule for that line unless you are absolutely sure and are aware of what future consequences may arise if/when you ever update the callback logic. Disabling the linter rule will potentially mask future issues if the implementation changes and you do actually want to run the effect more often. My rule for this occasion is to add a comment to my future self and other team members to be aware they should check the dependency array.

Example:

useEffect(() => {
  const timerId = setTimeout(() => navigate(props.nextPage), 3000);
 
  return () => clearTimeout(timerId);
 
  // NOTE: Run effect once on component mount. Re-check dependencies
  // if effect logic is updated.
  // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
Bubal answered 14/9, 2022 at 17:42 Comment(4)
This answer is misleading. The linter is not all-knowing and you should not follow it's suggestions every time. See my answer for the explanationAnalisaanalise
@Analisaanalise What exactly is misleading? The linter warns about missing dependencies, and you are free to decide what to ignore or not.Bubal
You said that "useEffect hook is missing dependencies". But there are valid cases where you do NOT want navigate as a dependency. You can't say that it should be added just because in most cases that is true. In case of this user's question I'd argue that it wont matter if dependencies are added or not.Analisaanalise
@Analisaanalise In any case navigate is an external (to the useEffect hook) dependency, and you'll get the linter warning if you omit navigate. Like my answer states though, you can disable the rule for that line to signify to the linter "No really, I know what I'm doing here." You're not wrong that the linter isn't very smart, but it is really good at spotting missing external dependencies. It can't understand what the dependencies mean, and it can't read your mind what you want the effect to be and when you really want it to run.Bubal
U
0

Since navigate is actually a dependency, just add it to the dependency array (The second argument in the useEffect() hook)

Don't worry, the function will still run on mount and it is a safe enough bet that navigate won't change and cause an unwanted 2nd setTimeout

To be really safe, you can actually put in code to make sure the setTimeout is run only once, but it is overkill anyway

Undermost answered 14/9, 2022 at 17:44 Comment(0)
E
-1

You can simply hide this warning by adding this:

useEffect(() => {
   setTimeout(() => navigate(props.nextPage), 3000);
  // eslint-disable-next-line
}, []);

This warning is displayed by eslint rules. You can read more about this at How to fix missing dependency warning when using useEffect React Hook

Eris answered 14/9, 2022 at 17:39 Comment(0)
M
-1

Depending on the structure of how you are wanting to set up the project, you can use a useRef hook to eliminate the needed dependency of the useNavigate hook. Or you can add it into the dependency array. Here is an example of doing that.

  const navigate = useRef(useNavigate());

  useEffect(() => {
    const timeout = setTimeout(() => navigate.current(props.nextPage), 3000);

    return () => {
      clearTimeout(timeout);
    };
  }, [props.nextPage]);
Mythology answered 14/9, 2022 at 17:45 Comment(0)
N
-1

Another option here would be to use the built in Navigate component. You can use a different state variable to decide whether to render your component or navigate away.

function Greeting(props) {
  const [navigateReady , setNavigateReady] = useState(false);
  
  useEffect(() => {
    setTimeout(() => setNavigateReady(true), 3000);
  }, []);

  if (navigateReady) {
    return <Navigate to={props.nextPage} />;
  }
  
  return (
    <div className="Greeting">
      <div>Hello World!</div>
    </div>
  );
}
Nakada answered 21/6, 2024 at 15:18 Comment(1)
The question was about the popper way to use navigate() inside an effect. Yes you could avoid calling the navigate() function directly by instead returning the <Navigate> component, which internally calls the navigate() function, but that is just more code for no reason.Analisaanalise

© 2022 - 2025 — McMap. All rights reserved.