Wrapping useState hook with custom setter and clean-up functionality

Viewed 2580

I'd like to create a custom React hook that

  1. wraps a state (created with useState),
  2. runs a clean-up function up when the component unmounts,
  3. executes a custom setter logic that derives a value,
  4. and exports the custom setter and the internal state value.
export function useCustomState() {
  const [value, setValue] = useState();

  useEffect(() => {
    return () => {
      // do cleanup
    };
  }, [value]);

  function customSetValue(newValue) {
    if (value) {
      // do cleanup for the previous value
    }

    const derivedValue = derive(newValue);
    setValue(derivedValue);
  }

  return [value, customSetValue];
}

I can achieve this with the code above. My problem arises when I use the returned values in an useEffect hook as a dependency, since the returned custom setter is always a new function reference.

const Component = () => {
  const [value, setValue] = useCustomState();
  useEffect(
    () => {
      setValue(simpleValue);
    },
    [setValue],
  );

  return <p>{ value }</p>;
};

When I don't include the setter as a dependency of the useEffect, the re-rendering stops, since the dependency does not change after a render. I can omit that reference and disable eslint for the line. This is one solution.

I'd like to know whether it is possible to create a custom referentially stable setter function?

I also have tried using useMemo that stores the custom setter function, but the dependencies for the memo still include the internal value and setValue references, since I want to do clean-up and set the new derived value. If the derived value is not the same for the same input, the dependency cycle will result in infinite re-rendering.

1 Answers

You can pass a function to setValue to remove the dependence on value; instead, the current value is passed as an argument. As a simpler alternative to useMemo, useCallback will give you a consistent function:

export function useCustomState() {
  const [value, setValue] = useState();

  useEffect(() => {
    return () => {
      // do cleanup
    };
  }, [value]);

  const customSetValue = useCallback((newValue) => {
    setValue((oldValue) => {
      if (oldValue) {
        // do cleanup for the previous value
      }
      const derivedValue = derive(newValue);
      return derivedValue;
    });
  }, []);

  return [value, customSetValue];
}
Related