skip to Main Content

I have a component with some internal state that is not being propagated throughout the application just on certain events.

const FunComponent = (initialDescription, onSave) => {
  const [description, setDescription] = useState('initialDescription');

  const handleDescriptionSave = () => {
    onSave(description);
  }

  return ( ... );
}

My issue is that the component can get unmounted by something higher up in the component tree, and at that point I would want to force-trigger onSave. Great – i thought – I can just write a useEffect with an empty dependency array, and it will be run on unmount.

The issue is that both the onSave prop and the description state is something my useEffect cleanup depends on.

If I add them to the dependency array, it will save with the previous description value on every description change. I don’t want that.
If I don’t add them to the dependency array, it will just use the initial onSave and description value on unmount.

const FunComponent = (initialDescription, onSave) => {
  const [description, setDescription] = useState(initialDescription);

  const handleDescriptionSave = () => {
    onSave(description);
  }

  useEffect(() => {
    return () => { onSave(description) };
  }, [onSave, description])

  return ( ... );
}

What hack I came up with is to store the callback in a ref, and have an effect to keep it up to date, and call the ref on unmount:

const FunComponent = (initialDescription, onSave) => {
  const [description, setDescription] = useState(initialDescription);

  const handleDescriptionSave = useCallback(() => {
    onSave(description);
  }, [onSave, description]);

  const handleDescriptionSaveRef = useRef(handleDescriptionSave);

  useEffect(() => {
    handleDescriptionSaveRef.current = handleDescriptionSave;
  }, [onSave, description]);

  useEffect(() => {
    return () => { handleDescriptionSaveRef.current(); };
  }, []);

  return ( ... );
}

But this seems super-duper hacky.

Is there a general recommended pattern for this I fail to recognise? Will this fail in some way? Is this a good solution?

2

Answers


  1. Interesting question.

    Your approach does not look as something bad. You can use special hook for it like this https://github.com/streamich/react-use/blob/master/src/useUnmount.ts

    In our case you can provide two args to hook: cb and args

    const useUnmount = (fn: () => any, ...args): void => {
      const fnRef = useRef(fn);
    
      // update the ref each render so if it change the newest callback will be invoked
      fnRef.current = () => fn(...args);
    
      useEffectOnce(() => () => fnRef.current());
    };
    
    Login or Signup to reply.
  2. Maybe you could store the callback directly into the ref without useCallback():

    const FunComponent = (initialDescription, onSave) => {
      const [description, setDescription] = useState(initialDescription);
    
      const handleDescriptionSaveRef = useRef();
    
      useEffect(() => {
        handleDescriptionSaveRef.current = () => onSave(description);
      }, [onSave, description]);
    
      useEffect(() => {
        return () => { handleDescriptionSaveRef.current(); };
      }, []);
    
      return ( ... );
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search