useMutableSource hook (#18000)

useMutableSource hook

useMutableSource() enables React components to safely and efficiently read from a mutable external source in Concurrent Mode. The API will detect mutations that occur during a render to avoid tearing and it will automatically schedule updates when the source is mutated.

RFC: reactjs/rfcs#147
This commit is contained in:
Brian Vaughn
2020-03-11 12:34:39 -07:00
committed by GitHub
parent 30a998debf
commit 322cdcd3ab
24 changed files with 2062 additions and 27 deletions
+31
View File
@@ -8,6 +8,9 @@
*/
import type {
MutableSource,
MutableSourceGetSnapshotFn,
MutableSourceSubscribeFn,
ReactContext,
ReactProviderType,
ReactEventResponder,
@@ -72,6 +75,16 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useDebugValue(null);
Dispatcher.useCallback(() => {});
Dispatcher.useMemo(() => null);
Dispatcher.useMutableSource(
{
_source: {},
_getVersion: () => 1,
_workInProgressVersionPrimary: null,
_workInProgressVersionSecondary: null,
},
() => null,
() => () => {},
);
} finally {
readHookLog = hookLog;
hookLog = [];
@@ -229,6 +242,23 @@ function useMemo<T>(
return value;
}
function useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
// useMutableSource() composes multiple hooks internally.
// Advance the current hook index the same number of times
// so that subsequent hooks have the right memoized state.
nextHook(); // MutableSource
nextHook(); // State
nextHook(); // Effect
nextHook(); // Effect
const value = getSnapshot(source._source);
hookLog.push({primitive: 'MutableSource', stackError: new Error(), value});
return value;
}
function useResponder(
responder: ReactEventResponder<any, any>,
listenerProps: Object,
@@ -299,6 +329,7 @@ const Dispatcher: DispatcherType = {
useState,
useResponder,
useTransition,
useMutableSource,
useDeferredValue,
useEvent,
};
@@ -785,4 +785,38 @@ describe('ReactHooksInspectionIntegration', () => {
},
]);
});
if (__EXPERIMENTAL__) {
it('should support composite useMutableSource hook', () => {
const mutableSource = React.createMutableSource({}, () => 1);
function Foo(props) {
React.useMutableSource(
mutableSource,
() => 'snapshot',
() => {},
);
React.useMemo(() => 'memo', []);
return <div />;
}
let renderer = ReactTestRenderer.create(<Foo />);
let childFiber = renderer.root.findByType(Foo)._currentFiber();
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
id: 0,
isStateEditable: false,
name: 'MutableSource',
value: 'snapshot',
subHooks: [],
},
{
id: 1,
isStateEditable: false,
name: 'Memo',
value: 'memo',
subHooks: [],
},
]);
});
}
});
@@ -13,6 +13,9 @@ import type {
} from 'react-reconciler/src/ReactFiberHooks';
import type {ThreadID} from './ReactThreadIDAllocator';
import type {
MutableSource,
MutableSourceGetSnapshotFn,
MutableSourceSubscribeFn,
ReactContext,
ReactEventResponderListener,
} from 'shared/ReactTypes';
@@ -461,6 +464,18 @@ function useResponder(responder, props): ReactEventResponderListener<any, any> {
};
}
// TODO Decide on how to implement this hook for server rendering.
// If a mutation occurs during render, consider triggering a Suspense boundary
// and falling back to client rendering.
function useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
resolveCurrentlyRenderingComponent();
return getSnapshot(source._source);
}
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
resolveCurrentlyRenderingComponent();
return value;
@@ -510,4 +525,6 @@ export const Dispatcher: DispatcherType = {
useDeferredValue,
useTransition,
useEvent,
// Subscriptions are not setup in a server environment.
useMutableSource,
};
+2 -2
View File
@@ -180,7 +180,7 @@ import {
markSpawnedWork,
requestCurrentTimeForUpdate,
retryDehydratedSuspenseBoundary,
scheduleWork,
scheduleUpdateOnFiber,
renderDidSuspendDelayIfPossible,
markUnprocessedUpdateTime,
} from './ReactFiberWorkLoop';
@@ -2121,7 +2121,7 @@ function updateDehydratedSuspenseComponent(
// at even higher pri.
let attemptHydrationAtExpirationTime = renderExpirationTime + 1;
suspenseState.retryTime = attemptHydrationAtExpirationTime;
scheduleWork(current, attemptHydrationAtExpirationTime);
scheduleUpdateOnFiber(current, attemptHydrationAtExpirationTime);
// TODO: Early abort this render.
} else {
// We have already tried to ping at a higher priority than we're rendering with
+4 -4
View File
@@ -53,7 +53,7 @@ import {readContext} from './ReactFiberNewContext';
import {
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleWork,
scheduleUpdateOnFiber,
} from './ReactFiberWorkLoop';
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
@@ -200,7 +200,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, expirationTime);
},
enqueueReplaceState(inst, payload, callback) {
const fiber = getInstance(inst);
@@ -224,7 +224,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, expirationTime);
},
enqueueForceUpdate(inst, callback) {
const fiber = getInstance(inst);
@@ -247,7 +247,7 @@ const classComponentUpdater = {
}
enqueueUpdate(fiber, update);
scheduleWork(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, expirationTime);
},
};
@@ -26,6 +26,7 @@ import type {
SuspenseListRenderState,
} from './ReactFiberSuspenseComponent';
import type {SuspenseContext} from './ReactFiberSuspenseContext';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource';
import {now} from './SchedulerWithReactIntegration';
@@ -662,6 +663,7 @@ function completeWork(
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const fiberRoot = (workInProgress.stateNode: FiberRoot);
if (fiberRoot.pendingContext) {
fiberRoot.context = fiberRoot.pendingContext;
+310 -2
View File
@@ -8,6 +8,9 @@
*/
import type {
MutableSource,
MutableSourceGetSnapshotFn,
MutableSourceSubscribeFn,
ReactEventResponder,
ReactContext,
ReactEventResponderListener,
@@ -17,6 +20,7 @@ import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {HookEffectTag} from './ReactHookEffectTags';
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
import type {FiberRoot} from './ReactFiberRoot';
import type {
ReactListenerEvent,
ReactListenerMap,
@@ -39,7 +43,8 @@ import {
Passive as HookPassive,
} from './ReactHookEffectTags';
import {
scheduleWork,
getWorkInProgressRoot,
scheduleUpdateOnFiber,
computeExpirationForFiber,
requestCurrentTimeForUpdate,
warnIfNotCurrentlyActingEffectsInDEV,
@@ -60,6 +65,14 @@ import {
runWithPriority,
getCurrentPriorityLevel,
} from './SchedulerWithReactIntegration';
import {
getPendingExpirationTime,
getWorkInProgressVersion,
markSourceAsDirty,
setPendingExpirationTime,
setWorkInProgressVersion,
warnAboutMultipleRenderersDEV,
} from './ReactMutableSource';
const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals;
@@ -103,6 +116,11 @@ export type Dispatcher = {|
useTransition(
config: SuspenseConfig | void | null,
): [(() => void) => void, boolean],
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot,
useEvent(event: ReactListenerEvent): ReactListenerMap,
|};
@@ -137,6 +155,7 @@ export type HookType =
| 'useResponder'
| 'useDeferredValue'
| 'useTransition'
| 'useMutableSource'
| 'useEvent';
let didWarnAboutMismatchedHooksForComponent;
@@ -855,6 +874,225 @@ function rerenderReducer<S, I, A>(
return [newState, dispatch];
}
type MutableSourceMemoizedState<Source, Snapshot> = {|
refs: {
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
},
source: MutableSource<any>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|};
function readFromUnsubcribedMutableSource<Source, Snapshot>(
root: FiberRoot,
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
): Snapshot {
if (__DEV__) {
warnAboutMultipleRenderersDEV(source);
}
const getVersion = source._getVersion;
const version = getVersion(source._source);
// Is it safe for this component to read from this source during the current render?
let isSafeToReadFromSource = false;
// Check the version first.
// If this render has already been started with a specific version,
// we can use it alone to determine if we can safely read from the source.
const currentRenderVersion = getWorkInProgressVersion(source);
if (currentRenderVersion !== null) {
isSafeToReadFromSource = currentRenderVersion === version;
} else {
// If there's no version, then we should fallback to checking the update time.
const pendingExpirationTime = getPendingExpirationTime(root);
if (pendingExpirationTime === NoWork) {
isSafeToReadFromSource = true;
} else {
// If the source has pending updates, we can use the current render's expiration
// time to determine if it's safe to read again from the source.
isSafeToReadFromSource =
pendingExpirationTime === NoWork ||
pendingExpirationTime >= renderExpirationTime;
}
if (isSafeToReadFromSource) {
// If it's safe to read from this source during the current render,
// store the version in case other components read from it.
// A changed version number will let those components know to throw and restart the render.
setWorkInProgressVersion(source, version);
}
}
if (isSafeToReadFromSource) {
return getSnapshot(source._source);
} else {
// This handles the special case of a mutable source being shared beween renderers.
// In that case, if the source is mutated between the first and second renderer,
// The second renderer don't know that it needs to reset the WIP version during unwind,
// (because the hook only marks sources as dirty if it's written to their WIP version).
// That would cause this tear check to throw again and eventually be visible to the user.
// We can avoid this infinite loop by explicitly marking the source as dirty.
//
// This can lead to tearing in the first renderer when it resumes,
// but there's nothing we can do about that (short of throwing here and refusing to continue the render).
markSourceAsDirty(source);
invariant(
false,
'Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue.',
);
}
}
function useMutableSource<Source, Snapshot>(
hook: Hook,
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
const root = ((getWorkInProgressRoot(): any): FiberRoot);
invariant(
root !== null,
'Expected a work-in-progress root. This is a bug in React. Please file an issue.',
);
const getVersion = source._getVersion;
const version = getVersion(source._source);
const dispatcher = ReactCurrentDispatcher.current;
let [snapshot, setSnapshot] = dispatcher.useState(() =>
readFromUnsubcribedMutableSource(root, source, getSnapshot),
);
// Grab a handle to the state hook as well.
// We use it to clear the pending update queue if we have a new source.
const stateHook = ((workInProgressHook: any): Hook);
const memoizedState = ((hook.memoizedState: any): MutableSourceMemoizedState<
Source,
Snapshot,
>);
const refs = memoizedState.refs;
const prevGetSnapshot = refs.getSnapshot;
const prevSource = memoizedState.source;
const prevSubscribe = memoizedState.subscribe;
const fiber = currentlyRenderingFiber;
hook.memoizedState = ({
refs,
source,
subscribe,
}: MutableSourceMemoizedState<Source, Snapshot>);
// Sync the values needed by our subscribe function after each commit.
dispatcher.useEffect(() => {
refs.getSnapshot = getSnapshot;
}, [getSnapshot]);
// If we got a new source or subscribe function,
// we'll need to subscribe in a passive effect,
// and also check for any changes that fire between render and subscribe.
dispatcher.useEffect(() => {
const handleChange = () => {
const latestGetSnapshot = refs.getSnapshot;
try {
setSnapshot(latestGetSnapshot(source._source));
// Record a pending mutable source update with the same expiration time.
const currentTime = requestCurrentTimeForUpdate();
const suspenseConfig = requestCurrentSuspenseConfig();
const expirationTime = computeExpirationForFiber(
currentTime,
fiber,
suspenseConfig,
);
setPendingExpirationTime(root, expirationTime);
} catch (error) {
// A selector might throw after a source mutation.
// e.g. it might try to read from a part of the store that no longer exists.
// In this case we should still schedule an update with React.
// Worst case the selector will throw again and then an error boundary will handle it.
setSnapshot(() => {
throw error;
});
}
};
const unsubscribe = subscribe(source._source, handleChange);
if (__DEV__) {
if (typeof unsubscribe !== 'function') {
console.error(
'Mutable source subscribe function must return an unsubscribe function.',
);
}
}
// Check for a possible change between when we last rendered and when we just subscribed.
const maybeNewVersion = getVersion(source._source);
if (!is(version, maybeNewVersion)) {
const maybeNewSnapshot = getSnapshot(source._source);
if (!is(snapshot, maybeNewSnapshot)) {
setSnapshot(maybeNewSnapshot);
}
}
return unsubscribe;
}, [source, subscribe]);
// If any of the inputs to useMutableSource change, reading is potentially unsafe.
//
// If either the source or the subscription have changed we can't can't trust the update queue.
// Maybe the source changed in a way that the old subscription ignored but the new one depends on.
//
// If the getSnapshot function changed, we also shouldn't rely on the update queue.
// It's possible that the underlying source was mutated between the when the last "change" event fired,
// and when the current render (with the new getSnapshot function) is processed.
//
// In both cases, we need to throw away pending udpates (since they are no longer relevant)
// and treat reading from the source as we do in the mount case.
if (
!is(prevSource, source) ||
!is(prevSubscribe, subscribe) ||
!is(prevGetSnapshot, getSnapshot)
) {
stateHook.baseQueue = null;
snapshot = readFromUnsubcribedMutableSource(root, source, getSnapshot);
stateHook.memoizedState = stateHook.baseState = snapshot;
}
return snapshot;
}
function mountMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
const hook = mountWorkInProgressHook();
hook.memoizedState = ({
refs: {
getSnapshot,
},
source,
subscribe,
}: MutableSourceMemoizedState<Source, Snapshot>);
return useMutableSource(hook, source, getSnapshot, subscribe);
}
function updateMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
const hook = updateWorkInProgressHook();
return useMutableSource(hook, source, getSnapshot, subscribe);
}
function mountState<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
@@ -1383,7 +1621,7 @@ function dispatchAction<S, A>(
warnIfNotCurrentlyActingUpdatesInDev(fiber);
}
}
scheduleWork(fiber, expirationTime);
scheduleUpdateOnFiber(fiber, expirationTime);
}
}
@@ -1474,6 +1712,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useResponder: throwInvalidHookError,
useDeferredValue: throwInvalidHookError,
useTransition: throwInvalidHookError,
useMutableSource: throwInvalidHookError,
useEvent: throwInvalidHookError,
};
@@ -1493,6 +1732,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useEvent: mountEventListener,
};
@@ -1512,6 +1752,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useEvent: updateEventListener,
};
@@ -1531,6 +1772,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useResponder: createDeprecatedResponderListener,
useDeferredValue: rerenderDeferredValue,
useTransition: rerenderTransition,
useMutableSource: updateMutableSource,
useEvent: updateEventListener,
};
@@ -1681,6 +1923,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
mountHookTypesDev();
@@ -1803,6 +2054,15 @@ if (__DEV__) {
updateHookTypesDev();
return mountTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
updateHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
@@ -1925,6 +2185,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
@@ -2047,6 +2316,15 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
updateHookTypesDev();
@@ -2183,6 +2461,16 @@ if (__DEV__) {
mountHookTypesDev();
return mountTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
warnInvalidHookAccess();
mountHookTypesDev();
return mountMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
@@ -2320,6 +2608,16 @@ if (__DEV__) {
updateHookTypesDev();
return updateTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
warnInvalidHookAccess();
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
@@ -2457,6 +2755,16 @@ if (__DEV__) {
updateHookTypesDev();
return rerenderTransition(config);
},
useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
currentHookNameInDev = 'useMutableSource';
warnInvalidHookAccess();
updateHookTypesDev();
return updateMutableSource(source, getSnapshot, subscribe);
},
useEvent(event: ReactListenerEvent): ReactListenerMap {
currentHookNameInDev = 'useEvent';
warnInvalidHookAccess();
+2 -2
View File
@@ -15,7 +15,7 @@ import type {ReactNodeList} from 'shared/ReactTypes';
import {
flushSync,
scheduleWork,
scheduleUpdateOnFiber,
flushPassiveEffects,
} from './ReactFiberWorkLoop';
import {updateContainer, syncUpdates} from './ReactFiberReconciler';
@@ -319,7 +319,7 @@ function scheduleFibersWithFamiliesRecursively(
fiber._debugNeedsRemount = true;
}
if (needsRemount || needsRender) {
scheduleWork(fiber, Sync);
scheduleUpdateOnFiber(fiber, Sync);
}
if (child !== null && !needsRemount) {
scheduleFibersWithFamiliesRecursively(
+9 -9
View File
@@ -51,7 +51,7 @@ import {injectInternals, onScheduleRoot} from './ReactFiberDevToolsHook';
import {
requestCurrentTimeForUpdate,
computeExpirationForFiber,
scheduleWork,
scheduleUpdateOnFiber,
flushRoot,
batchedEventUpdates,
batchedUpdates,
@@ -294,7 +294,7 @@ export function updateContainer(
}
enqueueUpdate(current, update);
scheduleWork(current, expirationTime);
scheduleUpdateOnFiber(current, expirationTime);
return expirationTime;
}
@@ -338,7 +338,7 @@ export function attemptSynchronousHydration(fiber: Fiber): void {
}
break;
case SuspenseComponent:
flushSync(() => scheduleWork(fiber, Sync));
flushSync(() => scheduleUpdateOnFiber(fiber, Sync));
// If we're still blocked after this, we need to increase
// the priority of any promises resolving within this
// boundary so that they next attempt also has higher pri.
@@ -377,7 +377,7 @@ export function attemptUserBlockingHydration(fiber: Fiber): void {
return;
}
let expTime = computeInteractiveExpiration(requestCurrentTimeForUpdate());
scheduleWork(fiber, expTime);
scheduleUpdateOnFiber(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
}
@@ -389,7 +389,7 @@ export function attemptContinuousHydration(fiber: Fiber): void {
// Suspense.
return;
}
scheduleWork(fiber, ContinuousHydration);
scheduleUpdateOnFiber(fiber, ContinuousHydration);
markRetryTimeIfNotHydrated(fiber, ContinuousHydration);
}
@@ -401,7 +401,7 @@ export function attemptHydrationAtCurrentPriority(fiber: Fiber): void {
}
const currentTime = requestCurrentTimeForUpdate();
const expTime = computeExpirationForFiber(currentTime, fiber, null);
scheduleWork(fiber, expTime);
scheduleUpdateOnFiber(fiber, expTime);
markRetryTimeIfNotHydrated(fiber, expTime);
}
@@ -484,7 +484,7 @@ if (__DEV__) {
// Shallow cloning props works as a workaround for now to bypass the bailout check.
fiber.memoizedProps = {...fiber.memoizedProps};
scheduleWork(fiber, Sync);
scheduleUpdateOnFiber(fiber, Sync);
}
};
@@ -494,11 +494,11 @@ if (__DEV__) {
if (fiber.alternate) {
fiber.alternate.pendingProps = fiber.pendingProps;
}
scheduleWork(fiber, Sync);
scheduleUpdateOnFiber(fiber, Sync);
};
scheduleUpdate = (fiber: Fiber) => {
scheduleWork(fiber, Sync);
scheduleUpdateOnFiber(fiber, Sync);
};
setSuspenseHandler = (newShouldSuspendImpl: Fiber => boolean) => {
+8
View File
@@ -26,6 +26,7 @@ import {
import {unstable_getThreadID} from 'scheduler/tracing';
import {NoPriority} from './SchedulerWithReactIntegration';
import {initializeUpdateQueue} from './ReactUpdateQueue';
import {clearPendingUpdates as clearPendingMutableSourceUpdates} from './ReactMutableSource';
export type PendingInteractionMap = Map<ExpirationTime, Set<Interaction>>;
@@ -74,6 +75,9 @@ type BaseFiberRootProperties = {|
// render again
lastPingedTime: ExpirationTime,
lastExpiredTime: ExpirationTime,
// Used by useMutableSource hook to avoid tearing within this root
// when external, mutable sources are read from during render.
mutableSourcePendingUpdateTime: ExpirationTime,
|};
// The following attributes are only used by interaction tracing builds.
@@ -123,6 +127,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
this.nextKnownPendingLevel = NoWork;
this.lastPingedTime = NoWork;
this.lastExpiredTime = NoWork;
this.mutableSourcePendingUpdateTime = NoWork;
if (enableSchedulerTracing) {
this.interactionThreadID = unstable_getThreadID();
@@ -249,6 +254,9 @@ export function markRootFinishedAtTime(
// Clear the expired time
root.lastExpiredTime = NoWork;
}
// Clear any pending updates that were just processed.
clearPendingMutableSourceUpdates(root, finishedExpirationTime);
}
export function markRootExpiredAtTime(
+3
View File
@@ -11,6 +11,7 @@ import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {SuspenseState} from './ReactFiberSuspenseComponent';
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource';
import {
ClassComponent,
HostRoot,
@@ -55,6 +56,7 @@ function unwindWork(
case HostRoot: {
popHostContainer(workInProgress);
popTopLevelLegacyContextObject(workInProgress);
resetMutableSourceWorkInProgressVersions();
const effectTag = workInProgress.effectTag;
invariant(
(effectTag & DidCapture) === NoEffect,
@@ -120,6 +122,7 @@ function unwindInterruptedWork(interruptedWork: Fiber) {
case HostRoot: {
popHostContainer(interruptedWork);
popTopLevelLegacyContextObject(interruptedWork);
resetMutableSourceWorkInProgressVersions();
break;
}
case HostComponent: {
+4 -1
View File
@@ -298,6 +298,10 @@ let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
// receive the same expiration time. Otherwise we get tearing.
let currentEventTime: ExpirationTime = NoWork;
export function getWorkInProgressRoot(): FiberRoot | null {
return workInProgressRoot;
}
export function requestCurrentTimeForUpdate() {
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// We're inside React, so it's fine to read the actual time.
@@ -451,7 +455,6 @@ export function scheduleUpdateOnFiber(
}
}
}
export const scheduleWork = scheduleUpdateOnFiber;
// This is split into a separate function so we can mark a fiber with pending
// work without treating it as a typical update that originates from an event;
+120
View File
@@ -0,0 +1,120 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ExpirationTime} from 'react-reconciler/src/ReactFiberExpirationTime';
import type {FiberRoot} from 'react-reconciler/src/ReactFiberRoot';
import type {MutableSource, MutableSourceVersion} from 'shared/ReactTypes';
import {isPrimaryRenderer} from './ReactFiberHostConfig';
import {NoWork} from './ReactFiberExpirationTime';
// Work in progress version numbers only apply to a single render,
// and should be reset before starting a new render.
// This tracks which mutable sources need to be reset after a render.
let workInProgressPrimarySources: Array<MutableSource<any>> = [];
let workInProgressSecondarySources: Array<MutableSource<any>> = [];
let rendererSigil;
if (__DEV__) {
// Used to detect multiple renderers using the same mutable source.
rendererSigil = {};
}
export function clearPendingUpdates(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
if (root.mutableSourcePendingUpdateTime <= expirationTime) {
root.mutableSourcePendingUpdateTime = NoWork;
}
}
export function getPendingExpirationTime(root: FiberRoot): ExpirationTime {
return root.mutableSourcePendingUpdateTime;
}
export function setPendingExpirationTime(
root: FiberRoot,
expirationTime: ExpirationTime,
): void {
root.mutableSourcePendingUpdateTime = expirationTime;
}
export function markSourceAsDirty(mutableSource: MutableSource<any>): void {
if (isPrimaryRenderer) {
workInProgressPrimarySources.push(mutableSource);
} else {
workInProgressSecondarySources.push(mutableSource);
}
}
export function resetWorkInProgressVersions(): void {
if (isPrimaryRenderer) {
for (let i = 0; i < workInProgressPrimarySources.length; i++) {
const mutableSource = workInProgressPrimarySources[i];
mutableSource._workInProgressVersionPrimary = null;
}
workInProgressPrimarySources.length = 0;
} else {
for (let i = 0; i < workInProgressSecondarySources.length; i++) {
const mutableSource = workInProgressSecondarySources[i];
mutableSource._workInProgressVersionSecondary = null;
}
workInProgressSecondarySources.length = 0;
}
}
export function getWorkInProgressVersion(
mutableSource: MutableSource<any>,
): null | MutableSourceVersion {
if (isPrimaryRenderer) {
return mutableSource._workInProgressVersionPrimary;
} else {
return mutableSource._workInProgressVersionSecondary;
}
}
export function setWorkInProgressVersion(
mutableSource: MutableSource<any>,
version: MutableSourceVersion,
): void {
if (isPrimaryRenderer) {
mutableSource._workInProgressVersionPrimary = version;
workInProgressPrimarySources.push(mutableSource);
} else {
mutableSource._workInProgressVersionSecondary = version;
workInProgressSecondarySources.push(mutableSource);
}
}
export function warnAboutMultipleRenderersDEV(
mutableSource: MutableSource<any>,
): void {
if (__DEV__) {
if (isPrimaryRenderer) {
if (mutableSource._currentPrimaryRenderer == null) {
mutableSource._currentPrimaryRenderer = rendererSigil;
} else if (mutableSource._currentPrimaryRenderer !== rendererSigil) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same mutable source. This is currently unsupported.',
);
}
} else {
if (mutableSource._currentSecondaryRenderer == null) {
mutableSource._currentSecondaryRenderer = rendererSigil;
} else if (mutableSource._currentSecondaryRenderer !== rendererSigil) {
console.error(
'Detected multiple renderers concurrently rendering the ' +
'same mutable source. This is currently unsupported.',
);
}
}
}
}
@@ -44,6 +44,7 @@ function loadModules({
ReactFeatureFlags.flushSuspenseFallbacksInTests = false;
ReactFeatureFlags.deferPassiveEffectCleanupDuringUnmount = deferPassiveEffectCleanupDuringUnmount;
ReactFeatureFlags.runAllPassiveEffectDestroysBeforeCreates = runAllPassiveEffectDestroysBeforeCreates;
ReactFeatureFlags.enableProfilerTimer = true;
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
@@ -1901,10 +1902,10 @@ function loadModules({
}
act(() => {
ReactNoop.render(
<React.Fragment>
<>
<Counter label="A" count={0} />
<Counter label="B" count={0} />
</React.Fragment>,
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough([
@@ -1922,10 +1923,10 @@ function loadModules({
act(() => {
ReactNoop.render(
<React.Fragment>
<>
<Counter label="A" count={1} />
<Counter label="B" count={1} />
</React.Fragment>,
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough([
@@ -1947,10 +1948,10 @@ function loadModules({
act(() => {
ReactNoop.render(
<React.Fragment>
<>
<Counter label="B" count={2} />
<Counter label="C" count={0} />
</React.Fragment>,
</>,
() => Scheduler.unstable_yieldValue('Sync effect'),
);
expect(Scheduler).toFlushAndYieldThrough([
File diff suppressed because it is too large Load Diff
+2
View File
@@ -26,6 +26,8 @@ export {
useReducer,
useRef,
useState,
useMutableSource,
createMutableSource,
Fragment,
Profiler,
StrictMode,
+2
View File
@@ -26,6 +26,8 @@ export {
useReducer,
useRef,
useState,
useMutableSource,
createMutableSource,
Fragment,
Profiler,
StrictMode,
+2
View File
@@ -55,6 +55,8 @@ export {
useReducer,
useRef,
useState,
useMutableSource,
createMutableSource,
Fragment,
Profiler,
StrictMode,
+2
View File
@@ -23,6 +23,8 @@ export {
useDebugValue,
useLayoutEffect,
useMemo,
useMutableSource,
createMutableSource,
useReducer,
useRef,
useState,
+4
View File
@@ -39,6 +39,7 @@ import {
useDebugValue,
useLayoutEffect,
useMemo,
useMutableSource,
useReducer,
useRef,
useState,
@@ -55,6 +56,7 @@ import {
jsxWithValidationStatic,
jsxWithValidationDynamic,
} from './ReactElementValidator';
import createMutableSource from './createMutableSource';
import ReactSharedInternals from './ReactSharedInternals';
import createFundamental from 'shared/createFundamentalComponent';
import createResponder from 'shared/createEventResponder';
@@ -81,6 +83,7 @@ const Children = {
export {
Children,
createMutableSource,
createRef,
Component,
PureComponent,
@@ -95,6 +98,7 @@ export {
useDebugValue,
useLayoutEffect,
useMemo,
useMutableSource,
useReducer,
useRef,
useState,
+12
View File
@@ -8,6 +8,9 @@
*/
import type {
MutableSource,
MutableSourceGetSnapshotFn,
MutableSourceSubscribeFn,
ReactContext,
ReactEventResponder,
ReactEventResponderListener,
@@ -177,3 +180,12 @@ export function useDeferredValue<T>(value: T, config: ?Object): T {
const dispatcher = resolveDispatcher();
return dispatcher.useDeferredValue(value, config);
}
export function useMutableSource<Source, Snapshot>(
source: MutableSource<Source>,
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
): Snapshot {
const dispatcher = resolveDispatcher();
return dispatcher.useMutableSource(source, getSnapshot, subscribe);
}
+29
View File
@@ -0,0 +1,29 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {MutableSource, MutableSourceGetVersionFn} from 'shared/ReactTypes';
export default function createMutableSource<Source: $NonMaybeType<mixed>>(
source: Source,
getVersion: MutableSourceGetVersionFn,
): MutableSource<Source> {
const mutableSource: MutableSource<Source> = {
_getVersion: getVersion,
_source: source,
_workInProgressVersionPrimary: null,
_workInProgressVersionSecondary: null,
};
if (__DEV__) {
mutableSource._currentPrimaryRenderer = null;
mutableSource._currentSecondaryRenderer = null;
}
return mutableSource;
}
+44
View File
@@ -190,3 +190,47 @@ export type ReactScopeInstance = {|
fiber: Object,
methods: null | ReactScopeMethods,
|};
// Mutable source version can be anything (e.g. number, string, immutable data structure)
// so long as it changes every time any part of the source changes.
export type MutableSourceVersion = $NonMaybeType<mixed>;
export type MutableSourceGetSnapshotFn<
Source: $NonMaybeType<mixed>,
Snapshot,
> = (source: Source) => Snapshot;
export type MutableSourceSubscribeFn<Source: $NonMaybeType<mixed>, Snapshot> = (
source: Source,
callback: (snapshot: Snapshot) => void,
) => () => void;
export type MutableSourceGetVersionFn = (
source: $NonMaybeType<mixed>,
) => MutableSourceVersion;
export type MutableSource<Source: $NonMaybeType<mixed>> = {|
_source: Source,
_getVersion: MutableSourceGetVersionFn,
// Tracks the version of this source at the time it was most recently read.
// Used to determine if a source is safe to read from before it has been subscribed to.
// Version number is only used during mount,
// since the mechanism for determining safety after subscription is expiration time.
//
// As a workaround to support multiple concurrent renderers,
// we categorize some renderers as primary and others as secondary.
// We only expect there to be two concurrent renderers at most:
// React Native (primary) and Fabric (secondary);
// React DOM (primary) and React ART (secondary).
// Secondary renderers store their context values on separate fields.
// We use the same approach for Context.
_workInProgressVersionPrimary: null | MutableSourceVersion,
_workInProgressVersionSecondary: null | MutableSourceVersion,
// DEV only
// Used to detect multiple renderers using the same mutable source.
_currentPrimaryRenderer?: Object | null,
_currentSecondaryRenderer?: Object | null,
|};
+3 -1
View File
@@ -346,5 +346,7 @@
"345": "Root did not complete. This is a bug in React.",
"346": "An event responder context was used outside of an event cycle.",
"347": "Maps are not valid as a React child (found: %s). Consider converting children to an array of keyed ReactElements instead.",
"348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React."
"348": "ensureListeningTo(): received a container that was not an element node. This is likely a bug in React.",
"349": "Expected a work-in-progress root. This is a bug in React. Please file an issue.",
"350": "Cannot read from mutable source during the current render without tearing. This is a bug in React. Please file an issue."
}