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 startTransitionReact.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 startTransitionReact.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!