Error: Abort fetching component for route: "/home" when trying to use router.replace within a useEffect

44 Views Asked by At

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:

  1. Set my default state for my search params on load
  2. Access the latest state within my search params
  3. 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.

1

There are 1 best solutions below

0
Hashan Hemachandra On

The issue seems to relate to how router.replace is called within an effect that triggers every time urlSearchParams changes. Due to the asynchronous nature of React's state updates and Next.js's router operations, router.replace may use stale state or query values.

Also If router.replace is called too frequently, it might lead to race conditions where the URL is updated with stale data. Debouncing these calls ensures that router.replace is only called after a certain period of inactivity, thus avoiding rapid, successive updates.

I have updated your code.

import { useState, useEffect, useCallback, useRef } from 'react';
import { useRouter } from 'next/navigation';
import _ from 'lodash';
import debounce from 'lodash/debounce'; // Make sure to import debounce

export const useUrlSearchParams = (defaults) => {
  const router = useRouter();
  const { query } = router;

  const [urlSearchParams, setUrlSearchParams] = useState(null);
  const pendingSearchParamsRef = useRef(null);

  // Debounce router.replace to avoid rapid updates
  const debouncedReplace = useCallback(
    debounce((nextQuery) => {
      router.replace({ query: nextQuery }, undefined, { scroll: false });
    }, 300), // Adjust debounce time as needed
    [],
  );

  useEffect(() => {
    if (router.isReady) {
      const { search } = query;
      const decodedSearchParams = search ? deserializeSearchParams(search) : defaults;
      setUrlSearchParams(decodedSearchParams);
      pendingSearchParamsRef.current = decodedSearchParams; // Set the initial ref state
    }
  }, [router.isReady, query, defaults]);

  useEffect(() => {
    if (urlSearchParams !== null && !_.isEqual(urlSearchParams, pendingSearchParamsRef.current)) {
      const serializedSearchParams = serializeSearchParams(urlSearchParams);
      if (query['search'] !== serializedSearchParams) {
        pendingSearchParamsRef.current = urlSearchParams;
        debouncedReplace({ ...query, search: serializedSearchParams });
      }
    }
  }, [urlSearchParams, query, router, debouncedReplace]);

  const updateUrlSearchParams = useCallback(
    (value) => {
      if (urlSearchParams) {
        const urlSearchParamsClone = _.cloneDeep(urlSearchParams);
        const mergedSearchParams = _.mergeWith(
          urlSearchParamsClone,
          value,
          (a, b) => Array.isArray(a) ? b : undefined,
        );
        setUrlSearchParams(mergedSearchParams);
      }
    },
    [urlSearchParams],
  );

  return [urlSearchParams, updateUrlSearchParams];
};