Don't list next/router
in useEffect
dependencies
Published about 2 months ago
It's a bad idea to include router
(returned from next/router
's useRouter
) in useEffect
dependencies.
router
is stable across renders except right after anything in your app calls router.push(...)
. Every router.push
call replaces the entire router
object with a brand new object, and as soon as React sees this new object in the useEffect
dependencies, it will run the effect again.
Often, this doesn't cause problems, because router.push
typically takes the user to a new page. Any useEffect
s from the old page won't run again—they'll just get cleaned up.
But when you implement something like router.push({ query: { page: 2 } })
to change query parameters without leaving the page, then you accidentally re-run all the useEffect
s on that page that have router
in their deps. It's a big problem for stuff like this:
import { useEffect } from 'react'
import { useRouter } from 'next/router'
function MyComponent() {
const router = useRouter()
useEffect(
() => {
// Do any kind of effectful work that should only happen
// once per page view, like tracking page views, setting up
// global event listeners, running animations, fetching data,
// etc.
router.push(...)
},
[
// Including `router` in this deps array can make this effect
// run more than once per page view.
router,
// ...someOtherDeps
]
)
return (...)
}
To make matters worse, the router
identity change itself won't trigger a React re-render. React will wait for the next piece of state to change, and then re-render. When or if React finally gets that new reason to re-render, it will notice the router
object has changed, and re-run your effects.
The simplest fix to this nasty issue is to remove router
from your deps, but the ESLint rule react-hooks/exhaustive-deps
will (correctly) catch that as a potential bug.
Don't disable the ESLint rule—it's incredibly useful! Instead, you can work around the entire router
problem by replacing router
with uppercase Router
, imported from next/router
like so:
import { useEffect } from 'react'
import Router from 'next/router'
function MyComponent() {
useEffect(
() => {
// Do any kind of effectful work that should only happen
// once per page view, like tracking page views, setting up
// global event listeners, running animations, fetching data,
// etc.
Router.push(...)
},
[
// ESLint will recognize `Router` as a global, stable object,
// and it will not ask you to include it in your deps array.
// ...someOtherDeps
]
)
return (...)
}
A final note for Next 15+ users: all of these router
behaviors apply for the Pages Router, but not the App Router. For the app router, you can import { useRouter } from 'next/navigation'
, and that router
object is always stable across renders, including full-page re-renders, even after someone calls router.push
.
This is the code you should write for the App Router:
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
function MyComponent() {
const router = useRouter()
useEffect(
() => {
// Do any kind of effectful work that should only happen
// once per page view, like tracking page views, setting up
// global event listeners, running animations, fetching data,
// etc.
router.push(...)
},
[
// Including `router` in this deps array is safe for the App Router.
router,
// ...someOtherDeps
]
)
return (...)
}