Should I use memoization or callback when passing a function as parameter of a custom hook?

I wrote a custom hook that accepts an async function as parameter.

The custom hook will call the function and return the result when done.

Should I memoize or useCallback when passing the function ?

Here’s a sample usage of my hook

    const {
        status,
        error,
        fire,
        data
    } = useAsyncOperation(
        () => {
            return Promise.resolve("Async result");
        },
        { autofire: true }
    );

What I fear is that inline anonymous function will change across renders and that the hook consider it as a new value (even if the function is the same).

How should I handle this ?

FYI: I’ve set up a code sandbox to illustrate the use of my custom hook

And the code of my hook is:

import { useCallback, useEffect, useState } from 'react';

type UseAsyncOperationResult<TResult> = {
    status: 'idle' | 'pending' | 'success' | 'error';
    fire: () => Promise<TResult>;
    error?: Error;
    data?: TResult;
};

type UseAsyncOperationOptions<TResult> = {
    onSuccess?: (result: TResult) => void;
    onError?: (error: Error) => void;
    autofire?: boolean;
};

export const useAsyncOperation = <TResult>(
    operation: () => Promise<TResult>,
    options: UseAsyncOperationOptions<TResult> = {}
): UseAsyncOperationResult<TResult> => {
    // TODO: use reducer to manage state in a cleaner way
    const [status, setStatus] = useState<'idle' | 'pending' | 'success' | 'error'>('idle');
    const [error, setError] = useState<Error | undefined>();
    const [data, setData] = useState<TResult | undefined>();
    const [autofired, setAutofired] = useState(false);

    const { onSuccess, onError, autofire } = options;

    const fire = useCallback(async () => {
        setStatus('pending');
        try {
            const result = await operation();
            setStatus('success');
            setData(result);
            if (onSuccess) onSuccess(result);
            return result;
        } catch (error) {
            setStatus('error');
            setError(error as Error);
            if (onError) onError(error as Error);
            return Promise.reject(error);
        }
    }, [onError, onSuccess, operation]);

    useEffect(() => {
        if (autofire && !autofired) {
            console.log('autofire');
            setAutofired(true);
            void fire();
        }
    }, [autofire, fire, autofired]);

    return {
        status,
        fire,
        error,
        data,
    };
};

>Solution :

Yes, you need to keep the reference of the callback that you are passing to the useAsyncOperation.

The way how to determine when you have to use useCallback or not for functions is to check if that function is tracked as dependency. And your custom hook tracks operation in useCallback, and the result is also tracked in useEffect.

If you do not keep the reference of your operation callback, fire will get a new reference on each render, since it tracks operation. And this will lead to trigger useEffect as well, since it tracks fire

Leave a Reply