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
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 an external reference (to the effect) so it should be included as a dependency, so this leaves the nextPage
prop value that should also 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
}, []);
The extended answer is that the navigate
function returned by the useNavigate
hook is sometimes stable and sometimes not.
export function useNavigate(): NavigateFunction { let { isDataRoute } = React.useContext(RouteContext); // Conditional usage is OK here because the usage of a data router is static // eslint-disable-next-line react-hooks/rules-of-hooks return isDataRoute ? useNavigateStable() : useNavigateUnstable(); }
The useNavigateUnstable
hook does have a dependency on the location.pathname
:
function useNavigateUnstable(): NavigateFunction {
...
let { pathname: locationPathname } = useLocation();
...
let navigate: NavigateFunction = React.useCallback(
(to: To | number, options: NavigateOptions = {}) => {
warning(activeRef.current, navigateEffectWarning);
// Short circuit here since if this happens on first render the navigate
// is useless because we haven't wired up our history listener yet
if (!activeRef.current) return;
if (typeof to === "number") {
navigator.go(to);
return;
}
let path = resolveTo(
to,
JSON.parse(routePathnamesJson),
locationPathname,
options.relative === "path"
);
// If we're operating within a basename, prepend it to the pathname prior
// to handing off to history (but only if we're not in a data router,
// otherwise it'll prepend the basename inside of the router).
// If this is a root navigation, then we navigate to the raw basename
// which allows the basename to have full control over the presence of a
// trailing slash on root links
if (dataRouterContext == null && basename !== "/") {
path.pathname =
path.pathname === "/"
? basename
: joinPaths([basename, path.pathname]);
}
(!!options.replace ? navigator.replace : navigator.push)(
path,
options.state,
options
);
},
[
basename,
navigator,
routePathnamesJson,
locationPathname,
dataRouterContext,
]
);
return navigate;
}
Whereas useNavigateStable
does not have this same dependency on the location pathname:
function useNavigateStable(): NavigateFunction {
let { router } = useDataRouterContext(DataRouterHook.UseNavigateStable);
let id = useCurrentRouteId(DataRouterStateHook.UseNavigateStable);
let activeRef = React.useRef(false);
useIsomorphicLayoutEffect(() => {
activeRef.current = true;
});
let navigate: NavigateFunction = React.useCallback(
async (to: To | number, options: NavigateOptions = {}) => {
warning(activeRef.current, navigateEffectWarning);
// Short circuit here since if this happens on first render the navigate
// is useless because we haven't wired up our router subscriber yet
if (!activeRef.current) return;
if (typeof to === "number") {
router.navigate(to);
} else {
await router.navigate(to, { fromRouteId: id, ...options });
}
},
[router, id]
);
return navigate;
}
If you are using a Data router (i.e. one created using createBrowserRouter
, createMemoryRouter
, etc, and rendered by RouterProvider
) the navigate
function is provided as a stable reference and is likely safe to always be added as a React hook dependency.