I'm currently working on a project using Next.js v13.3.0 where I'm storing state within my search params. I have a custom hook that I've created that allows me to:
- Set my default state for my search params on load
- Access the latest state within my search params
- Update my search params
I'm doing this using next's useRouter. and router.replace
here is the structure of the custom hook.
export const useUrlSearchParams = (defaults) => {
const router = useRouter();
const { query } = router;
const [urlSearchParams, setUrlSearchParams] = useState(null);
// Set searchParams when router is ready
useEffect(() => {
if (router.isReady) {
const { search } = query;
const decodedSearchParams = search
? deserializeSearchParams(search)
: (defaults);
setUrlSearchParams(decodedSearchParams);
}
}, [router.isReady, query, defaults]);
// Update URL when searchParams changes
useEffect(() => {
if (urlSearchParams !== null) {
const serializedSearchParams = serializeSearchParams(urlSearchParams);
if (query['search'] !== serializedSearchParams) {
query['search'] = serializedSearchParams;
router.replace({ query }, undefined, { scroll: false });
}
}
}, [urlSearchParams, query, router]);
const updateUrlSearchParams = useCallback(
(value) => {
if (urlSearchParams) {
// Need a deep clone made because merge below will update the object passed in.
const urlSearchParamsClone = _.cloneDeep(urlSearchParams);
// Merge in with a customizer to replace arrays instead of concat (default)
const mergedSearchParams = _.mergeWith(
urlSearchParamsClone,
value,
function (a, b) {
if (Array.isArray(a)) {
return b;
}
},
);
const serializedOriginal = serializeSearchParams(urlSearchParams);
const serializedUpdated = serializeSearchParams(mergedSearchParams);
if (serializedOriginal !== serializedUpdated) {
setUrlSearchParams(mergedSearchParams);
}
}
},
[urlSearchParams, setUrlSearchParams],
);
return [urlSearchParams, updateUrlSearchParams] as const;
};
The issue that I'm running into when trying to call updateUrlSerachParams is that router.replace can sometimes run more than once taking in a previous version of search params and replacing it with the already updated version.
I'm not sure how to resolve this issue.
The issue seems to relate to how
router.replaceis called within an effect that triggers every timeurlSearchParamschanges. Due to the asynchronous nature of React's state updates and Next.js's router operations,router.replacemay use stale state or query values.Also If
router.replaceis called too frequently, it might lead to race conditions where the URL is updated with stale data. Debouncing these calls ensures thatrouter.replaceis only called after a certain period of inactivity, thus avoiding rapid, successive updates.I have updated your code.