How to Create and Test a React Query Hook for Global Loading Indicators

serifcolakel

Serif COLAKEL

Posted on October 20, 2024

How to Create and Test a React Query Hook for Global Loading Indicators

React Query is a powerful tool for handling data fetching, caching, and synchronization in React applications. In this article, we'll create a custom hook using React Query's useIsFetching, useIsMutating, and useIsRestoring functions to determine if any service call is pending, allowing us to manage global loading states and show indicators. Then, we'll write unit tests using Jest to ensure the hook works as expected.

Prerequisites

Before we start, make sure you have the following installed:

  • React Query (@tanstack/react-query)
  • Jest (for testing)
  • React Testing Library (@testing-library/react-hooks) for testing hooks

If you don’t have these installed, you can add them via npm:

npm install @tanstack/react-query @testing-library/react-hooks jest
Enter fullscreen mode Exit fullscreen mode

Step 1: Creating the Custom Hook

First, let's create a custom hook called useServiceConfig that checks if any service call is pending:

import { useIsFetching, useIsMutating, useIsRestoring } from '@tanstack/react-query';
import { useMemo } from 'react';

const modes = {
    fetching: 'fetching',
    mutating: 'mutating',
    restoring: 'restoring',
    all: 'all',
} as const;

type TMode = keyof typeof modes;

/**
 * @name useServiceConfig
 * @description A custom hook that returns a boolean value indicating if any service call is pending.
 * @param {TMode} mode The mode to check for pending service calls. Default is `'all'`.
 * @returns {readonly [boolean]} A tuple containing a single boolean value indicating if any service call is pending.
 */
const useServiceConfig = (mode: TMode = modes.all): readonly [boolean] => {
    const isFetching = useIsFetching();
    const isMutating = useIsMutating();
    const isRestoring = useIsRestoring();

    const isPending = useMemo(() => {
        switch (mode) {
            case modes.fetching:
                return isFetching > 0;
            case modes.mutating:
                return isMutating > 0;
            case modes.restoring:
                return isRestoring;
            case modes.all:
            default:
                return isFetching > 0 || isMutating > 0 || isRestoring;
        }
    }, [mode, isFetching, isMutating, isRestoring]);

    return [isPending] as const;
};

export default useServiceConfig;
Enter fullscreen mode Exit fullscreen mode

Explanation

  • useIsFetching(): Returns the number of active queries currently being fetched.
  • useIsMutating(): Returns the number of ongoing mutations (e.g., POST, PUT, DELETE requests).
  • useIsRestoring(): Checks if React Query is restoring the cache from storage.

We combine these values using useMemo to determine if any of them indicate a pending state. The hook then returns a tuple containing this boolean value.

We use these functions to determine if any service call is pending. If any of these functions return a value greater than 0, we set isPending to true.

Step 2: Writing Unit Tests

Now that we have our hook, let's write unit tests using Jest to ensure it behaves as expected.

Setting Up the Tests

Create a file called useServiceConfig.test.ts (or .js if not using TypeScript). We'll use React Testing Library's renderHook utility to render our hook in a test environment.

import { renderHook } from '@testing-library/react-hooks';
import useServiceConfig from './useServiceConfig';
import { useIsFetching, useIsMutating, useIsRestoring } from '@tanstack/react-query';

jest.mock('@tanstack/react-query');

describe('useServiceConfig', () => {
    it('should return false when all statuses are negative or false for the default mode', () => {
        (useIsFetching as jest.Mock).mockReturnValue(0);
        (useIsMutating as jest.Mock).mockReturnValue(0);
        (useIsRestoring as jest.Mock).mockReturnValue(false);

        const { result } = renderHook(() => useServiceConfig());
        expect(result.current[0]).toBe(false);
    });

    it('should return true when fetchingStatus is greater than 0 in "fetching" mode', () => {
        (useIsFetching as jest.Mock).mockReturnValue(1);
        (useIsMutating as jest.Mock).mockReturnValue(0);
        (useIsRestoring as jest.Mock).mockReturnValue(false);

        const { result } = renderHook(() => useServiceConfig('fetching'));
        expect(result.current[0]).toBe(true);
    });

    it('should return true when mutatingStatus is greater than 0 in "mutating" mode', () => {
        (useIsFetching as jest.Mock).mockReturnValue(0);
        (useIsMutating as jest.Mock).mockReturnValue(1);
        (useIsRestoring as jest.Mock).mockReturnValue(false);

        const { result } = renderHook(() => useServiceConfig('mutating'));
        expect(result.current[0]).toBe(true);
    });

    it('should return true when restoringStatus is true in "restoring" mode', () => {
        (useIsFetching as jest.Mock).mockReturnValue(0);
        (useIsMutating as jest.Mock).mockReturnValue(0);
        (useIsRestoring as jest.Mock).mockReturnValue(true);

        const { result } = renderHook(() => useServiceConfig('restoring'));
        expect(result.current[0]).toBe(true);
    });

    it('should return true when any status is positive or true in "all" mode', () => {
        (useIsFetching as jest.Mock).mockReturnValue(1);
        (useIsMutating as jest.Mock).mockReturnValue(0);
        (useIsRestoring as jest.Mock).mockReturnValue(false);

        const { result } = renderHook(() => useServiceConfig('all'));
        expect(result.current[0]).toBe(true);
    });
});
Enter fullscreen mode Exit fullscreen mode

Explanation of the Tests

  • Mocking Dependencies:
    • We use jest.mock to mock the functions useIsFetching, useIsMutating, and useIsRestoring.
    • Mocking allows us to simulate different return values and control the behavior during tests.
  • Test Cases:
    • Default Mode:
      • ('all'): If all statuses are zero or false, the hook should return false.
    • Specific Modes:
      • 'fetching': If useIsFetching returns a value greater than 0, the hook should return true.
      • 'mutating': If useIsMutating returns a value greater than 0, the hook should return true.
      • 'restoring': If useIsRestoring returns true, the hook should also return true.
  • Running the Tests:

    • Run the tests using Jest:

      npm test
      

      You should see output indicating all tests have passed.

Conclusion

In this article, we built a custom React Query hook that checks the status of service calls based on different modes (fetching, mutating, restoring, or all). We then wrote and ran tests using Jest to ensure our hook behaves correctly in various scenarios. This approach helps manage global loading states, making it easier to show indicators in your application.

By following these steps, you can create similar hooks for different use cases and confidently test them.

Next Steps

  • Try extending the hook to accept other parameters, such as specific query keys, to customize its behavior further.
  • Explore more of React Query’s utilities to enhance your application's performance and user experience.

Happy coding!

💖 💪 🙅 🚩
serifcolakel
Serif COLAKEL

Posted on October 20, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related