javascriptreactjsredux-toolkitrtk-query

Handling stale callbacks in unwrap of mutations


I have problems with understanding how to properly handle a case. Let's say we have a simplified component:

const XComponent = ({ aCallback }) => {
  const [mutation] = useRtkQueryMutationHook()

  const doSomeAction = useCallback(() => {
    mutation().unwrap().then(() => {
      aCallback()
    })
  }, [mutation, aCallback])  

  return (
    <Button onClick={() => doSomeAction()} />
  )         
}

this component receives a callback, when a button is clicked, executes a POST, and after it finishes successfully it calls this callback.

As I understand if the aCallback changes after calling the mutation but before receiving POST response it will not be seen by .then() call, as it still sees the old, stale, aCallback.

How can I make it that the proper callback is called when the mutation finishes with success?

For example using useEffect would not achieve this effect, as useEffect would be called on each aCallback change, and not only on successful execution.

I could use useRef for holding the callback, but then if I have a lot of state that I want to pass to the callback, all it would need to be available in useRef too. Which feels werid to keep properties and local useState values in refs.

Of course sometimes I can refactor the code to not have such a requirement, but it is not always feasible.


Solution

  • I could use useRef for holding the callback, but then if I have a lot of state that I want to pass to the callback, all it would need to be available in useRef too. Which feels weird to keep properties and local useState values in refs.

    Your hunch is correct to use a React ref to cache a callback value that can be updated at any time. It's not atypical to do this, even for local component state values that need to be accessed in a callback closure.

    const XComponent = ({ aCallback }) => {
      const [mutation] = useRtkQueryMutationHook();
    
      const callbackRef = useRef();
    
      // Update callback ref value each time `aCallback` changes.
      useEffect(() => {
        callbackRef.current = aCallback;
      }, [aCallback]);
    
      const doSomeAction = useCallback(() => {
        mutation().unwrap()
          .then(() => {
            callbackRef.current?.();
          });
      }, [mutation, aCallback]); 
    
      return <Button onClick={doSomeAction} />;
    }
    

    If you've additional arguments that need to be passed to the callback, use the useEffect hook to capture these arguments in the callback scope as well.

    const XComponent = ({ aCallback }) => {
      const [mutation] = useRtkQueryMutationHook();
    
      const callbackRef = useRef();
    
      // Update callback ref value each time `aCallback` and any arguments change.
      useEffect(() => {
        callbackRef.current = () => aCallback(/* arguments */);
      }, [aCallback, /* arguments */]);
    
      const doSomeAction = useCallback(() => {
        mutation().unwrap()
          .then(() => {
            callbackRef.current?.();
          });
      }, [mutation, aCallback]); 
    
      return <Button onClick={doSomeAction} />;
    }