skip to Main Content

I’m encountering an issue with Next.js’s next/navigation router. I have a set of useEffects 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


  1. Chosen as BEST ANSWER

    I figured it out- using window.history.replaceState instead of router.replace prevents the useEffect from running again.


  2. Be careful with the dependencies you are putting in your hooks. You are not supposed to put router nor changeActivity. It’s not obvious in your example where calloutSet comes from. But your useEffect() function must be called when the window location changes, that’s for sure. So my recommendation is to put window.location in the dependencies of useEffect(). 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 use JSON.stringify() to pass a full copy of the object, so that if any property changes the hook is effectively called.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search