How does React.useDeferredValue() work internally?

In React concurrent mode, besides useTransition() and Suspense, there is one more API called useDeferredValue(), let’s see what it is and how it works.

What problem does useDeferredValue() try to solve?

There is detailed answer on React homepage, but the demo there is broken.

So I’ll just quickly demonstrate it here.

Without useDeferredValue()

Open this demo without useDeferredValue().

When the button is clicked, two API mocks for title and posts are fired, title API is quicker (300ms) but the posts API is slow(1000ms). Click the Next button, you can see both title and posts are switched after about 1000ms

This is because the button click handler is using useTransition(), since error will be thrown until the posts API returns data, both title and posts will be delayed.

This is not good, why don’t show title quicker?

With useDeferredValue()

Now open this demo with useDeferredValue().

Click the Next button again, you can see title is displayed first, followed by posts.

This is much better.

Create useDeferredValue() by ourselves

We’ve already covered how useTransition() work, simply speaking it just stops commiting when error is thrown.

To our problem here, when title API returns data React tries to rerender, but when posts is rendered, it throws because data is not ready.

jsx
function ProfilePage({ resource }) {
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</React.Suspense>
</React.Suspense>
);
}
jsx
function ProfilePage({ resource }) {
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={resource} />
</React.Suspense>
</React.Suspense>
);
}

Well, we can cache the resource for posts section to solve this problem, by state and update it in useEffect().

js
function useDeferredValue(value) {
const [state, setState] = React.useState(value);
React.useEffect(() => {
// since value might be promise which causes suspension
// we should wrap it with startTransition
React.startTransition(() => {
setState(value);
});
}, [value]);
return state;
}
js
function useDeferredValue(value) {
const [state, setState] = React.useState(value);
React.useEffect(() => {
// since value might be promise which causes suspension
// we should wrap it with startTransition
React.startTransition(() => {
setState(value);
});
}, [value]);
return state;
}

That’s it, now change the code to our implementation.

jsx
function ProfilePage({ resource }) {
const deferredResource = useDeferredValue(resource);
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={deferredResource} />
</React.Suspense>
</React.Suspense>
);
}
jsx
function ProfilePage({ resource }) {
const deferredResource = useDeferredValue(resource);
return (
<React.Suspense fallback={<h1>Loading profile...</h1>}>
<ProfileDetails resource={resource} />
<React.Suspense fallback={<h1>Loading posts...</h1>}>
<ProfileTimeline resource={deferredResource} />
</React.Suspense>
</React.Suspense>
);
}

Open demo with our own useDeferredValue(), it works just like React.useDeferredValue().

How does React.useDeferredValue() work?

Set up the debugger, we can find the source code, as usual, one for mount, one for update.

js
function mountDeferredValue<T>(value: T): T {
const [prevValue, setValue] = mountState(value);
mountEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}
function updateDeferredValue<T>(value: T): T {
const [prevValue, setValue] = updateState(value);
updateEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}
js
function mountDeferredValue<T>(value: T): T {
const [prevValue, setValue] = mountState(value);
mountEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}
function updateDeferredValue<T>(value: T): T {
const [prevValue, setValue] = updateState(value);
updateEffect(() => {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
setValue(value);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}, [value]);
return prevValue;
}

Wait, it is basically the same as what we wrote!! mountState and updateState is just implementation for useState.

You might wonder what the heck is ReactCurrentBatchConfig.transition, let’s see the source for startTransition() (source)

js
export function startTransition(scope: () => void) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
scope();
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}
js
export function startTransition(scope: () => void) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = 1;
try {
scope();
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
}
}

It is just the same!!! But what on earth is ReactCurrentBatchConfig.transition doing?

It is an internal implementation to tell React that updates inside of this function should be scheduled in transition lanes, which leads to NOT commiting when suspended.

The details could be found in my video.

Want to know more about how React works internally?
Check out my series - React Internals Deep Dive!

😳 Would you like to share my post to more people ?    

❮ Prev: The lifecycle of effect hooks in React

Next: Easily understand Contravariance of function arguments in TypeScript