Next.js custom asynchronous react hooks for fetching page data based on URL ID params

ยท

2 min read

If you're building a large Next.js/React app you're going to want to keep your code as DRY(Don't repeat yourself!) as possible.

That is where React custom hooks come in.

Consider the following.

You've got a dashboard that has separate pages to allow you to edit specific entries in your DB. E.g /manageCompany/xxxxxxxxxxxx/ and /managePermissions/xxxxxxxxxxxx/. Both of these pages will be fetching data from your API based on the xxxxxxxxxxxx id provided in the page URL. The code you're using on both pages will be near enough the same. If some logic changes then you're going to have to update many pages!

To solve this, you can use the custom hooks feature from React.

import { useEffect, useState } from 'react';
import { useRouter } from 'next/router';
import { prependPath } from '../lib/lang';
/**
 * Fetches the data for a DB entry based on the ID provided in the route. E.g
 * fetch company details for an id provided in /manageCompanies/xxxxxx
 * where xxxxxx is the Unique Dynamo DB for the item.
 * @param {Function} action The Async function used to fetch details
 *  for the id in the URl. Action must take an ID as the only parameter
 * @param {String} invalidRedirect The url to redirect users to if an ID is invalid
 * @param {...*} var_args Any other arguments that the action function needs to have passed in.
 */
export const useGetDataFromPathId = (invalidRedirect, action, ...otherArgs) => {
    const [data, setData] = useState({});
    const router = useRouter();
    const isMounted = useIsMounted();
    const [isLoading, setIsLoading] = useState(true);
    useEffect(() => {
        const fetchData = async () => {
            const response = await action(router.query.id, ...otherArgs);
            if (response === false || typeof response === undefined) {
                router.push(prependPath(invalidRedirect));
            }
            setIsLoading(false);
            setData(response);
        };
        if (isMounted && router?.query?.id) {
            fetchData();
        }
        // Passing Unique ID as a dependency
    }, [action, invalidRedirect, ...otherArgs, isMounted, router]);
    // Return 'isLoading' not the 'setIsLoading' function
    return [data, isLoading];
};
/**
 * Custom useIsMounted hook to cleanup components using async request in rendering of component
 * @returns {Boolean} Whether component is mounted.
 */
export const useIsMounted = () => {
    const [isMounted, setIsMounted] = useState(false);
    useEffect(() => {
        setIsMounted(true);
        return () => setIsMounted(false);
    }, []);
    return isMounted;
};

The above is the current contens of a lib/hooks.js file that contains my custom react hooks. You can then make use of these hooks by simply importing them and using in your components as follows:

import { useGetDataFromPathId } from '../../../../lib/hooks';

const EditCompany = (props) => {
  const [companyData, isLoadingCompanyData] = useGetDataFromPathId(
    "/manageCompanies",
    getCompanyDetails
  );
  return isLoadingCompanyData ? (
    "Loading..."
  ) : (
    <ManageCompanyForm companyData={companyData} />
  );
};

GLHF! ๐Ÿ˜Ž