Wednesday, 28 July 2021

Concurrent-safe version of useLatest in React?

There's a commonly used utility hook "useLatest", which returns a ref containing the latest value of the input. There are 2 common implementations:

const useLatest = <T>(value: T): { readonly current: T } => {
  const ref = useRef(value);
  ref.current = value;
  return ref;
};

From https://github.com/streamich/react-use/blob/master/src/useLatest.ts

const useLatest = <T extends any>(current: T) => {
  const storedValue = React.useRef(current)
  React.useEffect(() => {
    storedValue.current = current
  })
  return storedValue
}

From https://github.com/jaredLunde/react-hook/blob/master/packages/latest/src/index.tsx

The first version isn't suitable for React 18's concurrent mode, the second version will return the old value if used before useEffect runs (e.g. during render).

Is there a way to implement this that's both concurrent-safe and consistently returns the correct value?

Here's my attempt:

function useLatest<T>(val: T): React.MutableRefObject<T> {
  const ref = useRef({
    tempVal: val,
    committedVal: val,
    updateCount: 0,
  });
  ref.current.tempVal = val;
  const startingUpdateCount = ref.current.updateCount;

  useLayoutEffect(() => {
    ref.current.committedVal = ref.current.tempVal;
    ref.current.updateCount++;
  });

  return {
    get current() {
      // tempVal is from new render, committedVal is from old render.
      return ref.current.updateCount === startingUpdateCount
        ? ref.current.tempVal
        : ref.current.committedVal;
    },
    set current(newVal: T) {
      ref.current.tempVal = newVal;
    },
  };
}

This hasn't been thoroughly tested, just wrote it while writing this question, but it seems to work most of the time. It should be better than both versions above, but it has 2 issues: it returns a different object every time and it's still possible to be inconsistent in this scenario:

Render 1:

  1. ref1 = useLatest(val1)
  2. Create function1, which references ref1
  3. Commit (useLayoutEffect runs)

Render 2:

  1. useLatest(val2)
  2. Call function1

function1 will use val1, but it should use val2.



from Concurrent-safe version of useLatest in React?

No comments:

Post a Comment