-
Notifications
You must be signed in to change notification settings - Fork 47k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate useDeferredValue and useTransition #17058
Changes from all commits
9ea8084
3b44db5
10c30ee
2dd8b35
0067615
ab07c5a
7b7db2d
24e4bfa
4fdb6e0
c0ddda1
666e922
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,6 +19,7 @@ import type {HookEffectTag} from './ReactHookEffectTags'; | |
import type {SuspenseConfig} from './ReactFiberSuspenseConfig'; | ||
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration'; | ||
|
||
import * as Scheduler from 'scheduler'; | ||
import ReactSharedInternals from 'shared/ReactSharedInternals'; | ||
|
||
import {NoWork} from './ReactFiberExpirationTime'; | ||
|
@@ -54,7 +55,7 @@ import {markWorkInProgressReceivedUpdate} from './ReactFiberBeginWork'; | |
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig'; | ||
import {getCurrentPriorityLevel} from './SchedulerWithReactIntegration'; | ||
|
||
const {ReactCurrentDispatcher} = ReactSharedInternals; | ||
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; | ||
|
||
export type Dispatcher = { | ||
readContext<T>( | ||
|
@@ -92,6 +93,10 @@ export type Dispatcher = { | |
responder: ReactEventResponder<E, C>, | ||
props: Object, | ||
): ReactEventResponderListener<E, C>, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean], | ||
}; | ||
|
||
type Update<S, A> = { | ||
|
@@ -123,7 +128,9 @@ export type HookType = | |
| 'useMemo' | ||
| 'useImperativeHandle' | ||
| 'useDebugValue' | ||
| 'useResponder'; | ||
| 'useResponder' | ||
| 'useDeferredValue' | ||
| 'useTransition'; | ||
|
||
let didWarnAboutMismatchedHooksForComponent; | ||
if (__DEV__) { | ||
|
@@ -152,6 +159,10 @@ export type FunctionComponentUpdateQueue = { | |
lastEffect: Effect | null, | ||
}; | ||
|
||
export type TimeoutConfig = {| | ||
timeoutMs: number, | ||
|}; | ||
|
||
type BasicStateAction<S> = (S => S) | S; | ||
|
||
type Dispatch<A> = A => void; | ||
|
@@ -1117,6 +1128,96 @@ function updateMemo<T>( | |
return nextValue; | ||
} | ||
|
||
function mountDeferredValue<T>( | ||
value: T, | ||
config: TimeoutConfig | void | null, | ||
): T { | ||
const [prevValue, setValue] = mountState(value); | ||
mountEffect( | ||
() => { | ||
Scheduler.unstable_next(() => { | ||
const previousConfig = ReactCurrentBatchConfig.suspense; | ||
ReactCurrentBatchConfig.suspense = config === undefined ? null : config; | ||
try { | ||
setValue(value); | ||
} finally { | ||
ReactCurrentBatchConfig.suspense = previousConfig; | ||
} | ||
}); | ||
}, | ||
[value, config], | ||
); | ||
return prevValue; | ||
} | ||
|
||
function updateDeferredValue<T>( | ||
value: T, | ||
config: TimeoutConfig | void | null, | ||
): T { | ||
const [prevValue, setValue] = updateState(value); | ||
updateEffect( | ||
() => { | ||
Scheduler.unstable_next(() => { | ||
const previousConfig = ReactCurrentBatchConfig.suspense; | ||
ReactCurrentBatchConfig.suspense = config === undefined ? null : config; | ||
try { | ||
setValue(value); | ||
} finally { | ||
ReactCurrentBatchConfig.suspense = previousConfig; | ||
} | ||
}); | ||
}, | ||
[value, config], | ||
); | ||
return prevValue; | ||
} | ||
|
||
function mountTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
const [isPending, setPending] = mountState(false); | ||
const startTransition = mountCallback( | ||
callback => { | ||
setPending(true); | ||
Scheduler.unstable_next(() => { | ||
const previousConfig = ReactCurrentBatchConfig.suspense; | ||
ReactCurrentBatchConfig.suspense = config === undefined ? null : config; | ||
try { | ||
setPending(false); | ||
callback(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does the callback run as a potentially separate commit, or does being inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intention here is to:
As a result, you'll see pending immediately turn true, but then it turn false together with the actual state change you wanted to do. |
||
} finally { | ||
ReactCurrentBatchConfig.suspense = previousConfig; | ||
} | ||
}); | ||
}, | ||
[config, isPending], | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we pass |
||
); | ||
return [startTransition, isPending]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the only built-in hook producing a pair which returns a function as the first, instead of the second element. This will take some education. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That does seem odd. Should it be flipped? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The first argument is useful without the second but not the inverse. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn’t we nudge you to use the busy return value tho? |
||
} | ||
|
||
function updateTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
const [isPending, setPending] = updateState(false); | ||
const startTransition = updateCallback( | ||
callback => { | ||
setPending(true); | ||
Scheduler.unstable_next(() => { | ||
const previousConfig = ReactCurrentBatchConfig.suspense; | ||
ReactCurrentBatchConfig.suspense = config === undefined ? null : config; | ||
try { | ||
setPending(false); | ||
callback(); | ||
} finally { | ||
ReactCurrentBatchConfig.suspense = previousConfig; | ||
} | ||
}); | ||
}, | ||
[config, isPending], | ||
); | ||
return [startTransition, isPending]; | ||
} | ||
|
||
function dispatchAction<S, A>( | ||
fiber: Fiber, | ||
queue: UpdateQueue<S, A>, | ||
|
@@ -1272,6 +1373,8 @@ export const ContextOnlyDispatcher: Dispatcher = { | |
useState: throwInvalidHookError, | ||
useDebugValue: throwInvalidHookError, | ||
useResponder: throwInvalidHookError, | ||
useDeferredValue: throwInvalidHookError, | ||
useTransition: throwInvalidHookError, | ||
}; | ||
|
||
const HooksDispatcherOnMount: Dispatcher = { | ||
|
@@ -1288,6 +1391,8 @@ const HooksDispatcherOnMount: Dispatcher = { | |
useState: mountState, | ||
useDebugValue: mountDebugValue, | ||
useResponder: createResponderListener, | ||
useDeferredValue: mountDeferredValue, | ||
useTransition: mountTransition, | ||
}; | ||
|
||
const HooksDispatcherOnUpdate: Dispatcher = { | ||
|
@@ -1304,6 +1409,8 @@ const HooksDispatcherOnUpdate: Dispatcher = { | |
useState: updateState, | ||
useDebugValue: updateDebugValue, | ||
useResponder: createResponderListener, | ||
useDeferredValue: updateDeferredValue, | ||
useTransition: updateTransition, | ||
}; | ||
|
||
let HooksDispatcherOnMountInDEV: Dispatcher | null = null; | ||
|
@@ -1441,6 +1548,18 @@ if (__DEV__) { | |
mountHookTypesDev(); | ||
return createResponderListener(responder, props); | ||
}, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T { | ||
currentHookNameInDev = 'useDeferredValue'; | ||
mountHookTypesDev(); | ||
return mountDeferredValue(value, config); | ||
}, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
currentHookNameInDev = 'useTransition'; | ||
mountHookTypesDev(); | ||
return mountTransition(config); | ||
}, | ||
}; | ||
|
||
HooksDispatcherOnMountWithHookTypesInDEV = { | ||
|
@@ -1546,6 +1665,18 @@ if (__DEV__) { | |
updateHookTypesDev(); | ||
return createResponderListener(responder, props); | ||
}, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T { | ||
currentHookNameInDev = 'useDeferredValue'; | ||
updateHookTypesDev(); | ||
return mountDeferredValue(value, config); | ||
}, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
currentHookNameInDev = 'useTransition'; | ||
updateHookTypesDev(); | ||
return mountTransition(config); | ||
}, | ||
}; | ||
|
||
HooksDispatcherOnUpdateInDEV = { | ||
|
@@ -1651,6 +1782,18 @@ if (__DEV__) { | |
updateHookTypesDev(); | ||
return createResponderListener(responder, props); | ||
}, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T { | ||
currentHookNameInDev = 'useDeferredValue'; | ||
updateHookTypesDev(); | ||
return updateDeferredValue(value, config); | ||
}, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
currentHookNameInDev = 'useTransition'; | ||
updateHookTypesDev(); | ||
return updateTransition(config); | ||
}, | ||
}; | ||
|
||
InvalidNestedHooksDispatcherOnMountInDEV = { | ||
|
@@ -1768,6 +1911,20 @@ if (__DEV__) { | |
mountHookTypesDev(); | ||
return createResponderListener(responder, props); | ||
}, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T { | ||
currentHookNameInDev = 'useDeferredValue'; | ||
warnInvalidHookAccess(); | ||
mountHookTypesDev(); | ||
return mountDeferredValue(value, config); | ||
}, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
currentHookNameInDev = 'useTransition'; | ||
warnInvalidHookAccess(); | ||
mountHookTypesDev(); | ||
return mountTransition(config); | ||
}, | ||
}; | ||
|
||
InvalidNestedHooksDispatcherOnUpdateInDEV = { | ||
|
@@ -1885,5 +2042,19 @@ if (__DEV__) { | |
updateHookTypesDev(); | ||
return createResponderListener(responder, props); | ||
}, | ||
useDeferredValue<T>(value: T, config: TimeoutConfig | void | null): T { | ||
currentHookNameInDev = 'useDeferredValue'; | ||
warnInvalidHookAccess(); | ||
updateHookTypesDev(); | ||
return updateDeferredValue(value, config); | ||
}, | ||
useTransition( | ||
config: SuspenseConfig | void | null, | ||
): [(() => void) => void, boolean] { | ||
currentHookNameInDev = 'useTransition'; | ||
warnInvalidHookAccess(); | ||
updateHookTypesDev(); | ||
return updateTransition(config); | ||
}, | ||
}; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we not pass the
TimeoutConfig
toScheduler.unstable_next
in the mount case, when we do in the update case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice catch.
unstable_next
doesn't accept a second argument. It must have been a copypasta from withSuspenseConfig.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, it was accidental copy/paste. Deleted it! :)