A Concurrent Mode-Safe Version of useRef
Tags: React
Published: 2020-04-22 15:20:49

When you use React Hooks, components maintain internal states for hooks. For example, caches made by `useMemo` and objects returned by `useRef` are also residents of the internal state, as well as states controlled by the `useState` hook. During a rendering of a component, the internal states of that component are updated. The `useMemo` hook should be one of the easiest example of updates during a rendering. Caches of `useMemo` are updated immediately during a `useMemo` call, if necessary.

In [React's Concurrent Mode](https://reactjs.org/docs/concurrent-mode-intro.html), components have possibility of suspension. That is, a rendering of a component does not necessarily result in a DOM update (or other view updates if you are not using `react-dom`). Every time a component suspends, modifications made during the rendering that suspended are rolled back to the state before rendering. For example, a cache newly made by `useMemo` is discarded if that rendering suspends.

From this characteristic of Concurrent Mode, it follows that we ought to take extra care on usage of the `useRef` hook. The role of `useRef` is very simple; it always returns the same object (ref object; more accurately, an object that is made on the first rendering of that component). This object can be utilized for communication between rendering or any other side effects that originate from a component. The point is that, modifications made to the ref object is not rolled back even if a rendering suspends.

In an article [How To Properly Use the React useRef Hook in Concurrent Mode](https://medium.com/better-programming/how-to-properly-use-the-react-useref-hook-in-concurrent-mode-38c54543857b) by Daishi Kato, a usage of `useRef` where a ref object is modified during a rendering is regarded as a Bad Code:

```ts
const BadCounter = () => {
  const count = useRef(0);
  count.current += 1;
  return <div>count:{count.current}</div>;
};
```

The counter's value is increased every time the `BadCounter` is rendered. Of note is that, in Concurrent Mode, this may not match with how many times the contents of `BadCounter` is reflected to the DOM.

In a worse situation the current value of a ref object may interact with other hooks during a rendering. If such a rendering suspends, the component logic may fall into an inconsistent state where the ref object's value reflects the suspended rendering's state while other hooks' state are reset.

Therefore, to involve `useRef` in a rendering logic, we need a concurrent-mode safe version of `useRef`, whose value is automatically rolled back if  a rendering suspends. In other words, it is more like a variant of `useState` which does not trigger re-rendering.

Here it is:

```ts
type Raw<T> = {
  isRendering: boolean;
  comittedValue: T;
  currentValue: T;
  ref: { current: T };
};
export const useConcurrentModeSafeRef = <T>(initialValue: T) => {
  const rawRef = useRef<Raw<T>>();
  const raw: Raw<T> = rawRef.current ?? (
    rawRef.current ={
      isRendering: true,
      comittedValue: initialValue,
      currentValue: initialValue,
      ref: {
        get current() {
          if (raw.isRendering) {
            return raw.currentValue;
          } else {
            return raw.committedValue;
          }
        },
        set current(v) {
          if (!raw.isRendering) {
           raw.comittedValue = v;
          }
          raw.currentValue = v;
        }
      }
    }
  );
  raw.isRendering = true;
  Promise.resolve().then(()=> raw.isRendering = false)
  raw.currentValue = raw.comittedValue;
  useEffect(() => {
    raw.comittedValue = raw.currentValue;
  });

  return raw.ref;
};
```

This `useConcurrentModeSafeRef` returns an object with the same signature as `useRef`. The intended usage is to use `ref.current` as a storage that is persistent between renderings. Every time `useConcurrentModeSafeRef` is called, the `current` is reset to `raw.committedValue`. This value is updated only when a rendering succeeds; this behavior is realized with the help of `useEffect` hook. If a rendering suspends, `raw.committedValue` stays in the old state so that the changes made during that rendering are to be discarded.

The ref object returned by `useConcurrentModeSafeRef` behaves the same as `useRef` outside of renderings. Users can directly interact with `raw.committedValue` in such situations.

## Conclusion

This article explained a Concurrent Mode-safe variant of `useRef`. The key of the Concurrent Mode safety is that the value of ref objects are rolled back if a rendering suspends for aligned behavior with other hooks.