I’m encountering an issue with Next.js’s next/navigation router. I have a set of useEffect
s in my component, and for some reason calling router.replace
causes one of the effects to run a second a time, and in some cases just run infinitely. This causes a set of elements to play its transition animation twice (or just get stuck running infinitely) every time the user clicks a button that changes the activity.
const [selectedActivity, setSelectedActivity] = useState<Activity | null | undefined>(undefined)
const [customNames, setCustomNames] = useState<CustomNames>({})
const router = useRouter()
...
const changeActivity = useCallback((activity: Activity | null) => {
setSelectedActivity(activity)
const urlParams = new URLSearchParams(window.location.search);
if (activity) {
urlParams.set('activity', activity.id)
}
else {
urlParams.delete('activity')
}
// set query params to reflect the selected activity, or remove them if null
router.replace(`${window.location.pathname}?${urlParams.toString()}`)
}, [router])
...
useEffect(() => {
if (calloutSet) {
// read query params to see if activity is specified
const urlParams = new URLSearchParams(window.location.search);
const activityId = urlParams.get('activity');
const activity = calloutSet.activities.find(activity => activity.id == activityId)
changeActivity(activity ?? null)
// also populate custom names
const customNames: CustomNames = {}
// Convert urlParams.entries() to an array and iterate over it
Array.from(urlParams.entries()).forEach(([key, value]) => {
const imageId = parseInt(key)
if (isNaN(imageId)) return
const imageReference = calloutSet.allImages.find(image => image.id == imageId)
if (!imageReference) return
// If the name is the same as the default, remove it from the custom names
if (imageReference.name != value) {
customNames[imageId] = value
}
})
setCustomNames(customNames)
// if the callout set is not custom, attempt to load the custom names from local storage
if (!urlParams.has('isCustom') || urlParams.get('isCustom') != 'true') {
const customNamesJson = localStorage.getItem(`${calloutSet.id}.customNames`)
if (customNamesJson) {
setCustomNames(JSON.parse(customNamesJson))
}
}
}
}, [calloutSet, changeActivity])
By commenting out the router.replace
line in changeActivity, the erroneous re-rendering stopped (I still want the URL to update though). Removing calloutSet
and changeActivity
from the dependency list also worked, but ESLint would start yelling at me and it causes issues with my navbar search bar. I just want the useEffect to run once whenever the activity changes, and that’s it. Any ideas?
2
Answers
I figured it out- using
window.history.replaceState
instead ofrouter.replace
prevents theuseEffect
from running again.Be careful with the dependencies you are putting in your hooks. You are not supposed to put
router
norchangeActivity
. It’s not obvious in your example wherecalloutSet
comes from. But youruseEffect()
function must be called when the window location changes, that’s for sure. So my recommendation is to putwindow.location
in the dependencies ofuseEffect()
. Be careful also that there is no deep comparison of the dependencies, so make sure that if you pass variables by reference their reference changes every time. Sometimes I have to useJSON.stringify()
to pass a full copy of the object, so that if any property changes the hook is effectively called.