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