mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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:
@@ -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,
|
||||
};
|
||||
|
||||
+34
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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) => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -26,6 +26,8 @@ export {
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useMutableSource,
|
||||
createMutableSource,
|
||||
Fragment,
|
||||
Profiler,
|
||||
StrictMode,
|
||||
|
||||
@@ -26,6 +26,8 @@ export {
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useMutableSource,
|
||||
createMutableSource,
|
||||
Fragment,
|
||||
Profiler,
|
||||
StrictMode,
|
||||
|
||||
@@ -55,6 +55,8 @@ export {
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
useMutableSource,
|
||||
createMutableSource,
|
||||
Fragment,
|
||||
Profiler,
|
||||
StrictMode,
|
||||
|
||||
@@ -23,6 +23,8 @@ export {
|
||||
useDebugValue,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
createMutableSource,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
|};
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user