Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

like image 838
Leo Jiang Avatar asked Jul 24 '21 18:07

Leo Jiang


People also ask

How do you use concurrent mode in React?

React starts preparing the new screen in memory first — or, as our metaphor goes, “on a different branch”. So React can wait before updating the DOM so that more content can load. In Concurrent Mode, we can tell React to keep showing the old screen, fully interactive, with an inline loading indicator.

Should we use useRef in React?

In React you want to use the useRef hook or if you're in a React class component, you want to use createRef. The reason you don't want to use getElementById or querySelector is because you may be designing your React app to output multiple of the same ID's, which is a no no.

Where we use useRef in React?

The useRef Hook allows you to persist values between renders. It can be used to store a mutable value that does not cause a re-render when updated. It can be used to access a DOM element directly.


Video Answer


1 Answers

Here is what I think is correct:

const useLatest = <T extends any>(current: T) => {
  const storedValue = React.useRef(current)
  React.useLayoutEffect(() => {
    storedValue.current = current
  })
  return storedValue.current
}
like image 160
Izhaki Avatar answered Oct 23 '22 08:10

Izhaki