DEV Community

Linh Bui
Linh Bui

Posted on • Edited on

React: How does useDeferredValue and useTransition work ?

As we know that Concurrent was enabled by default in React 18.

To implement this feature, there are new 2 hooks that come into the game: useDeferredValue and useTransition

You might want to know how it work. Let’s find out

I. useDeferredValue

1. Mount phase:

In mount phase (first render), useDeferredValue will be treated as below :

function mountDeferredValue<T>(value: T): T {
  const hook = mountWorkInProgressHook();
  hook.memoizedState = value;
  return value;
}
Enter fullscreen mode Exit fullscreen mode

View Source

This mean it will save the current value into memoizedState and directly return it.

So on first render, deferredValue will be the same as input value.

2. Update phase:

When component have an update, useDeferredValue will be treated as below:

function updateDeferredValue<T>(value: T): T {
  const hook = updateWorkInProgressHook();
  const resolvedCurrentHook: Hook = (currentHook: any);
  const prevValue: T = resolvedCurrentHook.memoizedState;
  return updateDeferredValueImpl(hook, prevValue, value);
}
Enter fullscreen mode Exit fullscreen mode
function updateDeferredValueImpl<T>(hook: Hook, prevValue: T, value: T): T {
  const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
  if (shouldDeferValue) {
    if (!is(value, prevValue)) {
      const deferredLane = claimNextTransitionLane();
      currentlyRenderingFiber.lanes = mergeLanes(
        currentlyRenderingFiber.lanes,
        deferredLane,
      );
      markSkippedUpdateLanes(deferredLane);
      hook.baseState = true;
    }
    return prevValue;
  } else {
    if (hook.baseState) {
      hook.baseState = false;
      markWorkInProgressReceivedUpdate();
    }
    hook.memoizedState = value;
    return value;
  }
}
Enter fullscreen mode Exit fullscreen mode

View Source

  1. in updateDeferredValue, it will get the previos value that was already stored in memoizedState and put the previos value and current value as parameter to next function updateDeferredValueImpl

  2. in updateDeferredValueImpl, there will be a check if the current lanes contain a UrgentLanes

const shouldDeferValue = !includesOnlyNonUrgentLanes(renderLanes);
Enter fullscreen mode Exit fullscreen mode

Dig into includesOnlyNonUrgentLanes we will know UrgentLane are :

SyncLane | InputContinuousLane | DefaultLane;

So we can see that if current process contain a UrgentLane it will skip update DeferredValue and return the previos value immediately after schedule a render with Transition Lane (not prioritized).

Otherwise if current process contain only nonUrgentLanes, it will memoize and update the new value on useDeferredValue.

The flow is simply as below diagram:

image

II. useTransition

1. Mount phase

function mountTransition(): [
  boolean,
  (callback: () => void, options?: StartTransitionOptions) => void,
] {
  const [isPending, setPending] = mountState(false);
  // The `start` method never changes.
  const start = startTransition.bind(null, setPending);
  const hook = mountWorkInProgressHook();
  hook.memoizedState = start;
  return [isPending, start];
}
Enter fullscreen mode Exit fullscreen mode

View Source

Interesting, in mount phase, useTransition will use useState (mountState) to store its state "isPending".

Next, it will generate start function by bind setPending (dispatch setState) into startTransition, then put this function into hook. And return to the component.

Let's see what's startTransition do (I've removed all comments and warning code to make function shorter) :

function startTransition(setPending, callback) {
  const previousPriority = getCurrentUpdatePriority();
  setCurrentUpdatePriority(
    higherEventPriority(previousPriority, ContinuousEventPriority),
  );
  setPending(true);
  const prevTransition = ReactCurrentBatchConfig.transition;
  ReactCurrentBatchConfig.transition = 1;
  try {
    setPending(false);
    callback();
  } finally {
    setCurrentUpdatePriority(previousPriority);
    ReactCurrentBatchConfig.transition = prevTransition;
  }
}
Enter fullscreen mode Exit fullscreen mode

So at first, it will ensure the current priority is higher than ContinuousEventPriority, then set isPending to true to mark as transition start.
After that, it will save current value of ReactCurrentBatchConfig.transition into prevTransition and then set it to 1.

Guess what, setting ReactCurrentBatchConfig.transition to 1 will force put next updates into Transition Lane.

const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    if (currentEventTransitionLane === NoLane) {
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }
Enter fullscreen mode Exit fullscreen mode

You can check it in requestUpdateLane
After process callback, it will reset ReactCurrentBatchConfig into original by prevTransition variable.

2. Update phase

Update phase for useTransition is kind of simpler, since trigger function start was already memoized in hook.

function updateTransition(): [boolean, (() => void) => void] {
  const [isPending] = updateState(false);
  const hook = updateWorkInProgressHook();
  const start = hook.memoizedState;
  return [isPending, start];
}
Enter fullscreen mode Exit fullscreen mode

Conclusion:

By looking at their code, we can see that useTransition and useDeferredValue will basically put the process into Transition Lane.
Also, Concurrent does not mean React doing multi-task in the same time. It just mean React will process tasks with priority order. To do that, React has its Scheduler that may be discussed in another post.

Top comments (1)

Collapse
 
hokimquang profile image
HoKimQuang

Nice, that's what I was looking for. Thanks