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

Time:01-12

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;
    one rror?: (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, one rror, 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) one rror(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,
    };
};

CodePudding user response:

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

CodePudding user response:

Whenever the operation changes (ie - arrow function would change on each render), the useCallback would change and trigger the useEffect. However, calling the useEffect is fine, because after the 1st call the autofired flag would prevent other calls.

However, if still want to avoid re-creating the memoized fire function, and you don't want (and can't) force the users to always memoize the operation (via useCallback), use a ref to hold the operation:

const opRef = useRef(operation);

// update opRef on each render
useEffect(() => {
  opRef.current = operation;
})

const fire = useCallback(async() => {
  setStatus('pending');
  try {
    const result = await opRef.current(); // call the function that is stored in the ref
    setStatus('success');
    setData(result);
    if (onSuccess) onSuccess(result);
    return result;
  } catch (error) {
    setStatus('error');
    setError(error as Error);
    if (onError) one rror(error as Error);
    return Promise.reject(error);
  }
}, [onError, onSuccess]); // remove operation

CodePudding user response:

They both do the same thing but for semantics I'd say useCallback

  • Related