mirror of
https://github.com/facebook/react.git
synced 2026-02-26 18:58:05 +00:00
Add back useMutableSource temporarily (#22396)
Recoil uses useMutableSource behind a flag. I thought this was fine because Recoil isn't used in any concurrent roots, so the behavior would be the same, but it turns out that it is used by concurrent roots in a few places. I'm not expecting it to be hard to migrate to useSyncExternalStore, but to de-risk the change I'm going to roll it out gradually with a flag. In the meantime, I've added back the useMutableSource API.
This commit is contained in:
@@ -7,7 +7,13 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
ReactProviderType,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {
|
||||
Fiber,
|
||||
Dispatcher as DispatcherType,
|
||||
@@ -255,6 +261,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 useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -335,6 +358,7 @@ const Dispatcher: DispatcherType = {
|
||||
useRef,
|
||||
useState,
|
||||
useTransition,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useDeferredValue,
|
||||
useOpaqueIdentifier,
|
||||
|
||||
@@ -1019,6 +1019,43 @@ describe('ReactHooksInspectionIntegration', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should support composite useMutableSource hook', () => {
|
||||
const createMutableSource =
|
||||
React.createMutableSource || React.unstable_createMutableSource;
|
||||
const useMutableSource =
|
||||
React.useMutableSource || React.unstable_useMutableSource;
|
||||
|
||||
const mutableSource = createMutableSource({}, () => 1);
|
||||
function Foo(props) {
|
||||
useMutableSource(
|
||||
mutableSource,
|
||||
() => 'snapshot',
|
||||
() => {},
|
||||
);
|
||||
React.useMemo(() => 'memo', []);
|
||||
return <div />;
|
||||
}
|
||||
const renderer = ReactTestRenderer.create(<Foo />);
|
||||
const childFiber = renderer.root.findByType(Foo)._currentFiber();
|
||||
const 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: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// @gate experimental || www
|
||||
it('should support composite useSyncExternalStore hook', () => {
|
||||
const useSyncExternalStore = React.unstable_useSyncExternalStore;
|
||||
|
||||
27
packages/react-dom/src/client/ReactDOMRoot.js
vendored
27
packages/react-dom/src/client/ReactDOMRoot.js
vendored
@@ -8,7 +8,7 @@
|
||||
*/
|
||||
|
||||
import type {Container} from './ReactDOMHostConfig';
|
||||
import type {ReactNodeList} from 'shared/ReactTypes';
|
||||
import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
|
||||
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
export type RootType = {
|
||||
@@ -24,6 +24,7 @@ export type CreateRootOptions = {
|
||||
hydrationOptions?: {
|
||||
onHydrated?: (suspenseNode: Comment) => void,
|
||||
onDeleted?: (suspenseNode: Comment) => void,
|
||||
mutableSources?: Array<MutableSource<any>>,
|
||||
...
|
||||
},
|
||||
// END OF TODO
|
||||
@@ -34,6 +35,7 @@ export type CreateRootOptions = {
|
||||
|
||||
export type HydrateRootOptions = {
|
||||
// Hydration options
|
||||
hydratedSources?: Array<MutableSource<any>>,
|
||||
onHydrated?: (suspenseNode: Comment) => void,
|
||||
onDeleted?: (suspenseNode: Comment) => void,
|
||||
// Options for all roots
|
||||
@@ -59,6 +61,7 @@ import {
|
||||
createContainer,
|
||||
updateContainer,
|
||||
findHostInstanceWithNoPortals,
|
||||
registerMutableSourceForHydration,
|
||||
} from 'react-reconciler/src/ReactFiberReconciler';
|
||||
import invariant from 'shared/invariant';
|
||||
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
|
||||
@@ -126,6 +129,11 @@ export function createRoot(
|
||||
const hydrate = options != null && options.hydrate === true;
|
||||
const hydrationCallbacks =
|
||||
(options != null && options.hydrationOptions) || null;
|
||||
const mutableSources =
|
||||
(options != null &&
|
||||
options.hydrationOptions != null &&
|
||||
options.hydrationOptions.mutableSources) ||
|
||||
null;
|
||||
// END TODO
|
||||
|
||||
const isStrictMode = options != null && options.unstable_strictMode === true;
|
||||
@@ -151,6 +159,15 @@ export function createRoot(
|
||||
container.nodeType === COMMENT_NODE ? container.parentNode : container;
|
||||
listenToAllSupportedEvents(rootContainerElement);
|
||||
|
||||
// TODO: Delete this path
|
||||
if (mutableSources) {
|
||||
for (let i = 0; i < mutableSources.length; i++) {
|
||||
const mutableSource = mutableSources[i];
|
||||
registerMutableSourceForHydration(root, mutableSource);
|
||||
}
|
||||
}
|
||||
// END TODO
|
||||
|
||||
return new ReactDOMRoot(root);
|
||||
}
|
||||
|
||||
@@ -168,6 +185,7 @@ export function hydrateRoot(
|
||||
// For now we reuse the whole bag of options since they contain
|
||||
// the hydration callbacks.
|
||||
const hydrationCallbacks = options != null ? options : null;
|
||||
const mutableSources = (options != null && options.hydratedSources) || null;
|
||||
const isStrictMode = options != null && options.unstable_strictMode === true;
|
||||
|
||||
let concurrentUpdatesByDefaultOverride = null;
|
||||
@@ -190,6 +208,13 @@ export function hydrateRoot(
|
||||
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
|
||||
listenToAllSupportedEvents(container);
|
||||
|
||||
if (mutableSources) {
|
||||
for (let i = 0; i < mutableSources.length; i++) {
|
||||
const mutableSource = mutableSources[i];
|
||||
registerMutableSourceForHydration(root, mutableSource);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the initial children
|
||||
updateContainer(initialChildren, root, null, null);
|
||||
|
||||
|
||||
@@ -9,7 +9,12 @@
|
||||
|
||||
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import type PartialRenderer from './ReactPartialRenderer';
|
||||
|
||||
import {validateContextBounds} from './ReactPartialRendererContext';
|
||||
@@ -461,6 +466,18 @@ export function useCallback<T>(
|
||||
return useMemo(() => callback, deps);
|
||||
}
|
||||
|
||||
// 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 useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -527,6 +544,8 @@ export const Dispatcher: DispatcherType = {
|
||||
useDeferredValue,
|
||||
useTransition,
|
||||
useOpaqueIdentifier,
|
||||
// Subscriptions are not setup in a server environment.
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
};
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {Lanes, Lane} from './ReactFiberLane.new';
|
||||
import type {MutableSource} from 'shared/ReactTypes';
|
||||
import type {
|
||||
SuspenseState,
|
||||
SuspenseListRenderState,
|
||||
@@ -144,6 +145,7 @@ import {
|
||||
isSuspenseInstancePending,
|
||||
isSuspenseInstanceFallback,
|
||||
registerSuspenseInstanceRetry,
|
||||
supportsHydration,
|
||||
isPrimaryRenderer,
|
||||
supportsPersistence,
|
||||
getOffscreenContainerProps,
|
||||
@@ -218,6 +220,7 @@ import {
|
||||
RetryAfterError,
|
||||
NoContext,
|
||||
} from './ReactFiberWorkLoop.new';
|
||||
import {setWorkInProgressVersion} from './ReactMutableSource.new';
|
||||
import {
|
||||
requestCacheFromPool,
|
||||
pushCacheProvider,
|
||||
@@ -1297,6 +1300,21 @@ function updateHostRoot(current, workInProgress, renderLanes) {
|
||||
// We always try to hydrate. If this isn't a hydration pass there won't
|
||||
// be any children to hydrate which is effectively the same thing as
|
||||
// not hydrating.
|
||||
|
||||
if (supportsHydration) {
|
||||
const mutableSourceEagerHydrationData =
|
||||
root.mutableSourceEagerHydrationData;
|
||||
if (mutableSourceEagerHydrationData != null) {
|
||||
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
|
||||
const mutableSource = ((mutableSourceEagerHydrationData[
|
||||
i
|
||||
]: any): MutableSource<any>);
|
||||
const version = mutableSourceEagerHydrationData[i + 1];
|
||||
setWorkInProgressVersion(mutableSource, version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const child = mountChildFibers(
|
||||
workInProgress,
|
||||
null,
|
||||
|
||||
@@ -12,6 +12,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
|
||||
import type {Fiber, FiberRoot} from './ReactInternalTypes';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
import type {Lanes, Lane} from './ReactFiberLane.old';
|
||||
import type {MutableSource} from 'shared/ReactTypes';
|
||||
import type {
|
||||
SuspenseState,
|
||||
SuspenseListRenderState,
|
||||
@@ -144,6 +145,7 @@ import {
|
||||
isSuspenseInstancePending,
|
||||
isSuspenseInstanceFallback,
|
||||
registerSuspenseInstanceRetry,
|
||||
supportsHydration,
|
||||
isPrimaryRenderer,
|
||||
supportsPersistence,
|
||||
getOffscreenContainerProps,
|
||||
@@ -218,6 +220,7 @@ import {
|
||||
RetryAfterError,
|
||||
NoContext,
|
||||
} from './ReactFiberWorkLoop.old';
|
||||
import {setWorkInProgressVersion} from './ReactMutableSource.old';
|
||||
import {
|
||||
requestCacheFromPool,
|
||||
pushCacheProvider,
|
||||
@@ -1297,6 +1300,21 @@ function updateHostRoot(current, workInProgress, renderLanes) {
|
||||
// We always try to hydrate. If this isn't a hydration pass there won't
|
||||
// be any children to hydrate which is effectively the same thing as
|
||||
// not hydrating.
|
||||
|
||||
if (supportsHydration) {
|
||||
const mutableSourceEagerHydrationData =
|
||||
root.mutableSourceEagerHydrationData;
|
||||
if (mutableSourceEagerHydrationData != null) {
|
||||
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
|
||||
const mutableSource = ((mutableSourceEagerHydrationData[
|
||||
i
|
||||
]: any): MutableSource<any>);
|
||||
const version = mutableSourceEagerHydrationData[i + 1];
|
||||
setWorkInProgressVersion(mutableSource, version);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const child = mountChildFibers(
|
||||
workInProgress,
|
||||
null,
|
||||
|
||||
@@ -30,6 +30,8 @@ import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
|
||||
import type {OffscreenState} from './ReactFiberOffscreenComponent';
|
||||
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
|
||||
|
||||
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
|
||||
|
||||
import {now} from './Scheduler';
|
||||
|
||||
import {
|
||||
@@ -852,6 +854,7 @@ function completeWork(
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
popTopLevelLegacyContextObject(workInProgress);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
if (fiberRoot.pendingContext) {
|
||||
fiberRoot.context = fiberRoot.pendingContext;
|
||||
fiberRoot.pendingContext = null;
|
||||
|
||||
@@ -30,6 +30,8 @@ import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
|
||||
import type {OffscreenState} from './ReactFiberOffscreenComponent';
|
||||
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
|
||||
|
||||
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
|
||||
|
||||
import {now} from './Scheduler';
|
||||
|
||||
import {
|
||||
@@ -852,6 +854,7 @@ function completeWork(
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
popTopLevelLegacyContextObject(workInProgress);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
if (fiberRoot.pendingContext) {
|
||||
fiberRoot.context = fiberRoot.pendingContext;
|
||||
fiberRoot.pendingContext = null;
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
|
||||
import type {Lanes, Lane} from './ReactFiberLane.new';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
@@ -45,6 +50,7 @@ import {
|
||||
intersectLanes,
|
||||
isTransitionLane,
|
||||
markRootEntangled,
|
||||
markRootMutableRead,
|
||||
NoTimestamp,
|
||||
} from './ReactFiberLane.new';
|
||||
import {
|
||||
@@ -96,6 +102,12 @@ import {
|
||||
makeClientIdInDEV,
|
||||
makeOpaqueHydratingObject,
|
||||
} from './ReactFiberHostConfig';
|
||||
import {
|
||||
getWorkInProgressVersion,
|
||||
markSourceAsDirty,
|
||||
setWorkInProgressVersion,
|
||||
warnAboutMultipleRenderersDEV,
|
||||
} from './ReactMutableSource.new';
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
import {markStateUpdateScheduled} from './SchedulingProfiler';
|
||||
@@ -935,6 +947,289 @@ function rerenderReducer<S, I, A>(
|
||||
return [newState, dispatch];
|
||||
}
|
||||
|
||||
type MutableSourceMemoizedState<Source, Snapshot> = {|
|
||||
refs: {
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
setSnapshot: Snapshot => void,
|
||||
},
|
||||
source: MutableSource<any>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
|};
|
||||
|
||||
function readFromUnsubscribedMutableSource<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) {
|
||||
// It's safe to read if the store hasn't been mutated since the last time
|
||||
// we read something.
|
||||
isSafeToReadFromSource = currentRenderVersion === version;
|
||||
} else {
|
||||
// If there's no version, then this is the first time we've read from the
|
||||
// source during the current render pass, so we need to do a bit more work.
|
||||
// What we need to determine is if there are any hooks that already
|
||||
// subscribed to the source, and if so, whether there are any pending
|
||||
// mutations that haven't been synchronized yet.
|
||||
//
|
||||
// If there are no pending mutations, then `root.mutableReadLanes` will be
|
||||
// empty, and we know we can safely read.
|
||||
//
|
||||
// If there *are* pending mutations, we may still be able to safely read
|
||||
// if the currently rendering lanes are inclusive of the pending mutation
|
||||
// lanes, since that guarantees that the value we're about to read from
|
||||
// the source is consistent with the values that we read during the most
|
||||
// recent mutation.
|
||||
isSafeToReadFromSource = isSubsetOfLanes(
|
||||
renderLanes,
|
||||
root.mutableReadLanes,
|
||||
);
|
||||
|
||||
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) {
|
||||
const snapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof snapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
} else {
|
||||
// This handles the special case of a mutable source being shared between 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);
|
||||
|
||||
// Intentioally throw an error to force React to retry synchronously. During
|
||||
// the synchronous retry, it will block interleaved mutations, so we should
|
||||
// get a consistent read. Therefore, the following error should never be
|
||||
// visible to the user.
|
||||
|
||||
// We expect this error not to be thrown during the synchronous retry,
|
||||
// because we blocked interleaved mutations.
|
||||
invariant(
|
||||
false,
|
||||
'Cannot read from mutable source during the current render without tearing. This may be 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;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [currentSnapshot, setSnapshot] = dispatcher.useState(() =>
|
||||
readFromUnsubscribedMutableSource(root, source, getSnapshot),
|
||||
);
|
||||
let snapshot = currentSnapshot;
|
||||
|
||||
// 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 subscription handler after each commit.
|
||||
dispatcher.useEffect(() => {
|
||||
refs.getSnapshot = getSnapshot;
|
||||
|
||||
// Normally the dispatch function for a state hook never changes,
|
||||
// but this hook recreates the queue in certain cases to avoid updates from stale sources.
|
||||
// handleChange() below needs to reference the dispatch function without re-subscribing,
|
||||
// so we use a ref to ensure that it always has the latest version.
|
||||
refs.setSnapshot = setSnapshot;
|
||||
|
||||
// Check for a possible change between when we last rendered now.
|
||||
const maybeNewVersion = getVersion(source._source);
|
||||
if (!is(version, maybeNewVersion)) {
|
||||
const maybeNewSnapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof maybeNewSnapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is(snapshot, maybeNewSnapshot)) {
|
||||
setSnapshot(maybeNewSnapshot);
|
||||
|
||||
const lane = requestUpdateLane(fiber);
|
||||
markRootMutableRead(root, lane);
|
||||
}
|
||||
// If the source mutated between render and now,
|
||||
// there may be state updates already scheduled from the old source.
|
||||
// Entangle the updates so that they render in the same batch.
|
||||
markRootEntangled(root, root.mutableReadLanes);
|
||||
}
|
||||
}, [getSnapshot, source, subscribe]);
|
||||
|
||||
// If we got a new source or subscribe function, re-subscribe in a passive effect.
|
||||
dispatcher.useEffect(() => {
|
||||
const handleChange = () => {
|
||||
const latestGetSnapshot = refs.getSnapshot;
|
||||
const latestSetSnapshot = refs.setSnapshot;
|
||||
|
||||
try {
|
||||
latestSetSnapshot(latestGetSnapshot(source._source));
|
||||
|
||||
// Record a pending mutable source update with the same expiration time.
|
||||
const lane = requestUpdateLane(fiber);
|
||||
|
||||
markRootMutableRead(root, lane);
|
||||
} 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.
|
||||
latestSetSnapshot(
|
||||
(() => {
|
||||
throw error;
|
||||
}: any),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = subscribe(source._source, handleChange);
|
||||
if (__DEV__) {
|
||||
if (typeof unsubscribe !== 'function') {
|
||||
console.error(
|
||||
'Mutable source subscribe function must return an unsubscribe function.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 updates (since they are no longer relevant)
|
||||
// and treat reading from the source as we do in the mount case.
|
||||
if (
|
||||
!is(prevGetSnapshot, getSnapshot) ||
|
||||
!is(prevSource, source) ||
|
||||
!is(prevSubscribe, subscribe)
|
||||
) {
|
||||
// Create a new queue and setState method,
|
||||
// So if there are interleaved updates, they get pushed to the older queue.
|
||||
// When this becomes current, the previous queue and dispatch method will be discarded,
|
||||
// including any interleaving updates that occur.
|
||||
const newQueue: UpdateQueue<Snapshot, BasicStateAction<Snapshot>> = {
|
||||
pending: null,
|
||||
interleaved: null,
|
||||
lanes: NoLanes,
|
||||
dispatch: null,
|
||||
lastRenderedReducer: basicStateReducer,
|
||||
lastRenderedState: snapshot,
|
||||
};
|
||||
newQueue.dispatch = setSnapshot = (dispatchAction.bind(
|
||||
null,
|
||||
currentlyRenderingFiber,
|
||||
newQueue,
|
||||
): any);
|
||||
stateHook.queue = newQueue;
|
||||
stateHook.baseQueue = null;
|
||||
snapshot = readFromUnsubscribedMutableSource(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,
|
||||
setSnapshot: (null: any),
|
||||
},
|
||||
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 mountSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2035,6 +2330,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
useDebugValue: throwInvalidHookError,
|
||||
useDeferredValue: throwInvalidHookError,
|
||||
useTransition: throwInvalidHookError,
|
||||
useMutableSource: throwInvalidHookError,
|
||||
useSyncExternalStore: throwInvalidHookError,
|
||||
useOpaqueIdentifier: throwInvalidHookError,
|
||||
|
||||
@@ -2061,6 +2357,7 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
useDebugValue: mountDebugValue,
|
||||
useDeferredValue: mountDeferredValue,
|
||||
useTransition: mountTransition,
|
||||
useMutableSource: mountMutableSource,
|
||||
useSyncExternalStore: mountSyncExternalStore,
|
||||
useOpaqueIdentifier: mountOpaqueIdentifier,
|
||||
|
||||
@@ -2087,6 +2384,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
useDebugValue: updateDebugValue,
|
||||
useDeferredValue: updateDeferredValue,
|
||||
useTransition: updateTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useSyncExternalStore: updateSyncExternalStore,
|
||||
useOpaqueIdentifier: updateOpaqueIdentifier,
|
||||
|
||||
@@ -2113,6 +2411,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
useDebugValue: updateDebugValue,
|
||||
useDeferredValue: rerenderDeferredValue,
|
||||
useTransition: rerenderTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useSyncExternalStore: mountSyncExternalStore,
|
||||
useOpaqueIdentifier: rerenderOpaqueIdentifier,
|
||||
|
||||
@@ -2262,6 +2561,15 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2394,6 +2702,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2526,6 +2843,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2659,6 +2985,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return rerenderTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2805,6 +3140,16 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2953,6 +3298,16 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -3102,6 +3457,16 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return rerenderTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
|
||||
@@ -7,7 +7,12 @@
|
||||
* @flow
|
||||
*/
|
||||
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes';
|
||||
import type {Lanes, Lane} from './ReactFiberLane.old';
|
||||
import type {HookFlags} from './ReactHookEffectTags';
|
||||
@@ -45,6 +50,7 @@ import {
|
||||
intersectLanes,
|
||||
isTransitionLane,
|
||||
markRootEntangled,
|
||||
markRootMutableRead,
|
||||
NoTimestamp,
|
||||
} from './ReactFiberLane.old';
|
||||
import {
|
||||
@@ -96,6 +102,12 @@ import {
|
||||
makeClientIdInDEV,
|
||||
makeOpaqueHydratingObject,
|
||||
} from './ReactFiberHostConfig';
|
||||
import {
|
||||
getWorkInProgressVersion,
|
||||
markSourceAsDirty,
|
||||
setWorkInProgressVersion,
|
||||
warnAboutMultipleRenderersDEV,
|
||||
} from './ReactMutableSource.old';
|
||||
import {getIsRendering} from './ReactCurrentFiber';
|
||||
import {logStateUpdateScheduled} from './DebugTracing';
|
||||
import {markStateUpdateScheduled} from './SchedulingProfiler';
|
||||
@@ -935,6 +947,289 @@ function rerenderReducer<S, I, A>(
|
||||
return [newState, dispatch];
|
||||
}
|
||||
|
||||
type MutableSourceMemoizedState<Source, Snapshot> = {|
|
||||
refs: {
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
setSnapshot: Snapshot => void,
|
||||
},
|
||||
source: MutableSource<any>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
|};
|
||||
|
||||
function readFromUnsubscribedMutableSource<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) {
|
||||
// It's safe to read if the store hasn't been mutated since the last time
|
||||
// we read something.
|
||||
isSafeToReadFromSource = currentRenderVersion === version;
|
||||
} else {
|
||||
// If there's no version, then this is the first time we've read from the
|
||||
// source during the current render pass, so we need to do a bit more work.
|
||||
// What we need to determine is if there are any hooks that already
|
||||
// subscribed to the source, and if so, whether there are any pending
|
||||
// mutations that haven't been synchronized yet.
|
||||
//
|
||||
// If there are no pending mutations, then `root.mutableReadLanes` will be
|
||||
// empty, and we know we can safely read.
|
||||
//
|
||||
// If there *are* pending mutations, we may still be able to safely read
|
||||
// if the currently rendering lanes are inclusive of the pending mutation
|
||||
// lanes, since that guarantees that the value we're about to read from
|
||||
// the source is consistent with the values that we read during the most
|
||||
// recent mutation.
|
||||
isSafeToReadFromSource = isSubsetOfLanes(
|
||||
renderLanes,
|
||||
root.mutableReadLanes,
|
||||
);
|
||||
|
||||
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) {
|
||||
const snapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof snapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
return snapshot;
|
||||
} else {
|
||||
// This handles the special case of a mutable source being shared between 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);
|
||||
|
||||
// Intentioally throw an error to force React to retry synchronously. During
|
||||
// the synchronous retry, it will block interleaved mutations, so we should
|
||||
// get a consistent read. Therefore, the following error should never be
|
||||
// visible to the user.
|
||||
|
||||
// We expect this error not to be thrown during the synchronous retry,
|
||||
// because we blocked interleaved mutations.
|
||||
invariant(
|
||||
false,
|
||||
'Cannot read from mutable source during the current render without tearing. This may be 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;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
let [currentSnapshot, setSnapshot] = dispatcher.useState(() =>
|
||||
readFromUnsubscribedMutableSource(root, source, getSnapshot),
|
||||
);
|
||||
let snapshot = currentSnapshot;
|
||||
|
||||
// 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 subscription handler after each commit.
|
||||
dispatcher.useEffect(() => {
|
||||
refs.getSnapshot = getSnapshot;
|
||||
|
||||
// Normally the dispatch function for a state hook never changes,
|
||||
// but this hook recreates the queue in certain cases to avoid updates from stale sources.
|
||||
// handleChange() below needs to reference the dispatch function without re-subscribing,
|
||||
// so we use a ref to ensure that it always has the latest version.
|
||||
refs.setSnapshot = setSnapshot;
|
||||
|
||||
// Check for a possible change between when we last rendered now.
|
||||
const maybeNewVersion = getVersion(source._source);
|
||||
if (!is(version, maybeNewVersion)) {
|
||||
const maybeNewSnapshot = getSnapshot(source._source);
|
||||
if (__DEV__) {
|
||||
if (typeof maybeNewSnapshot === 'function') {
|
||||
console.error(
|
||||
'Mutable source should not return a function as the snapshot value. ' +
|
||||
'Functions may close over mutable values and cause tearing.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!is(snapshot, maybeNewSnapshot)) {
|
||||
setSnapshot(maybeNewSnapshot);
|
||||
|
||||
const lane = requestUpdateLane(fiber);
|
||||
markRootMutableRead(root, lane);
|
||||
}
|
||||
// If the source mutated between render and now,
|
||||
// there may be state updates already scheduled from the old source.
|
||||
// Entangle the updates so that they render in the same batch.
|
||||
markRootEntangled(root, root.mutableReadLanes);
|
||||
}
|
||||
}, [getSnapshot, source, subscribe]);
|
||||
|
||||
// If we got a new source or subscribe function, re-subscribe in a passive effect.
|
||||
dispatcher.useEffect(() => {
|
||||
const handleChange = () => {
|
||||
const latestGetSnapshot = refs.getSnapshot;
|
||||
const latestSetSnapshot = refs.setSnapshot;
|
||||
|
||||
try {
|
||||
latestSetSnapshot(latestGetSnapshot(source._source));
|
||||
|
||||
// Record a pending mutable source update with the same expiration time.
|
||||
const lane = requestUpdateLane(fiber);
|
||||
|
||||
markRootMutableRead(root, lane);
|
||||
} 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.
|
||||
latestSetSnapshot(
|
||||
(() => {
|
||||
throw error;
|
||||
}: any),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const unsubscribe = subscribe(source._source, handleChange);
|
||||
if (__DEV__) {
|
||||
if (typeof unsubscribe !== 'function') {
|
||||
console.error(
|
||||
'Mutable source subscribe function must return an unsubscribe function.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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 updates (since they are no longer relevant)
|
||||
// and treat reading from the source as we do in the mount case.
|
||||
if (
|
||||
!is(prevGetSnapshot, getSnapshot) ||
|
||||
!is(prevSource, source) ||
|
||||
!is(prevSubscribe, subscribe)
|
||||
) {
|
||||
// Create a new queue and setState method,
|
||||
// So if there are interleaved updates, they get pushed to the older queue.
|
||||
// When this becomes current, the previous queue and dispatch method will be discarded,
|
||||
// including any interleaving updates that occur.
|
||||
const newQueue: UpdateQueue<Snapshot, BasicStateAction<Snapshot>> = {
|
||||
pending: null,
|
||||
interleaved: null,
|
||||
lanes: NoLanes,
|
||||
dispatch: null,
|
||||
lastRenderedReducer: basicStateReducer,
|
||||
lastRenderedState: snapshot,
|
||||
};
|
||||
newQueue.dispatch = setSnapshot = (dispatchAction.bind(
|
||||
null,
|
||||
currentlyRenderingFiber,
|
||||
newQueue,
|
||||
): any);
|
||||
stateHook.queue = newQueue;
|
||||
stateHook.baseQueue = null;
|
||||
snapshot = readFromUnsubscribedMutableSource(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,
|
||||
setSnapshot: (null: any),
|
||||
},
|
||||
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 mountSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2035,6 +2330,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
|
||||
useDebugValue: throwInvalidHookError,
|
||||
useDeferredValue: throwInvalidHookError,
|
||||
useTransition: throwInvalidHookError,
|
||||
useMutableSource: throwInvalidHookError,
|
||||
useSyncExternalStore: throwInvalidHookError,
|
||||
useOpaqueIdentifier: throwInvalidHookError,
|
||||
|
||||
@@ -2061,6 +2357,7 @@ const HooksDispatcherOnMount: Dispatcher = {
|
||||
useDebugValue: mountDebugValue,
|
||||
useDeferredValue: mountDeferredValue,
|
||||
useTransition: mountTransition,
|
||||
useMutableSource: mountMutableSource,
|
||||
useSyncExternalStore: mountSyncExternalStore,
|
||||
useOpaqueIdentifier: mountOpaqueIdentifier,
|
||||
|
||||
@@ -2087,6 +2384,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
|
||||
useDebugValue: updateDebugValue,
|
||||
useDeferredValue: updateDeferredValue,
|
||||
useTransition: updateTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useSyncExternalStore: updateSyncExternalStore,
|
||||
useOpaqueIdentifier: updateOpaqueIdentifier,
|
||||
|
||||
@@ -2113,6 +2411,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
|
||||
useDebugValue: updateDebugValue,
|
||||
useDeferredValue: rerenderDeferredValue,
|
||||
useTransition: rerenderTransition,
|
||||
useMutableSource: updateMutableSource,
|
||||
useSyncExternalStore: mountSyncExternalStore,
|
||||
useOpaqueIdentifier: rerenderOpaqueIdentifier,
|
||||
|
||||
@@ -2262,6 +2561,15 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2394,6 +2702,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2526,6 +2843,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2659,6 +2985,15 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return rerenderTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2805,6 +3140,16 @@ if (__DEV__) {
|
||||
mountHookTypesDev();
|
||||
return mountTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
mountHookTypesDev();
|
||||
return mountMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -2953,6 +3298,16 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return updateTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -3102,6 +3457,16 @@ if (__DEV__) {
|
||||
updateHookTypesDev();
|
||||
return rerenderTransition();
|
||||
},
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot {
|
||||
currentHookNameInDev = 'useMutableSource';
|
||||
warnInvalidHookAccess();
|
||||
updateHookTypesDev();
|
||||
return updateMutableSource(source, getSnapshot, subscribe);
|
||||
},
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
|
||||
@@ -283,9 +283,9 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
|
||||
// time it takes to show the final state, which is what they are actually
|
||||
// waiting for.
|
||||
//
|
||||
// For those exceptions where entanglement is semantically important, we
|
||||
// should ensure that there is no partial work at the time we apply the
|
||||
// entanglement.
|
||||
// For those exceptions where entanglement is semantically important, like
|
||||
// useMutableSource, we should ensure that there is no partial work at the
|
||||
// time we apply the entanglement.
|
||||
const entangledLanes = root.entangledLanes;
|
||||
if (entangledLanes !== NoLanes) {
|
||||
const entanglements = root.entanglements;
|
||||
@@ -617,6 +617,10 @@ export function markRootPinged(
|
||||
root.pingedLanes |= root.suspendedLanes & pingedLanes;
|
||||
}
|
||||
|
||||
export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
|
||||
root.mutableReadLanes |= updateLane & root.pendingLanes;
|
||||
}
|
||||
|
||||
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
|
||||
|
||||
@@ -627,6 +631,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
root.pingedLanes = 0;
|
||||
|
||||
root.expiredLanes &= remainingLanes;
|
||||
root.mutableReadLanes &= remainingLanes;
|
||||
|
||||
root.entangledLanes &= remainingLanes;
|
||||
|
||||
|
||||
@@ -283,9 +283,9 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
|
||||
// time it takes to show the final state, which is what they are actually
|
||||
// waiting for.
|
||||
//
|
||||
// For those exceptions where entanglement is semantically important, we
|
||||
// should ensure that there is no partial work at the time we apply the
|
||||
// entanglement.
|
||||
// For those exceptions where entanglement is semantically important, like
|
||||
// useMutableSource, we should ensure that there is no partial work at the
|
||||
// time we apply the entanglement.
|
||||
const entangledLanes = root.entangledLanes;
|
||||
if (entangledLanes !== NoLanes) {
|
||||
const entanglements = root.entanglements;
|
||||
@@ -617,6 +617,10 @@ export function markRootPinged(
|
||||
root.pingedLanes |= root.suspendedLanes & pingedLanes;
|
||||
}
|
||||
|
||||
export function markRootMutableRead(root: FiberRoot, updateLane: Lane) {
|
||||
root.mutableReadLanes |= updateLane & root.pendingLanes;
|
||||
}
|
||||
|
||||
export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
const noLongerPendingLanes = root.pendingLanes & ~remainingLanes;
|
||||
|
||||
@@ -627,6 +631,7 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) {
|
||||
root.pingedLanes = 0;
|
||||
|
||||
root.expiredLanes &= remainingLanes;
|
||||
root.mutableReadLanes &= remainingLanes;
|
||||
|
||||
root.entangledLanes &= remainingLanes;
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ import {
|
||||
findBoundingRects as findBoundingRects_old,
|
||||
focusWithin as focusWithin_old,
|
||||
observeVisibleRects as observeVisibleRects_old,
|
||||
registerMutableSourceForHydration as registerMutableSourceForHydration_old,
|
||||
runWithPriority as runWithPriority_old,
|
||||
getCurrentUpdatePriority as getCurrentUpdatePriority_old,
|
||||
} from './ReactFiberReconciler.old';
|
||||
@@ -82,6 +83,7 @@ import {
|
||||
findBoundingRects as findBoundingRects_new,
|
||||
focusWithin as focusWithin_new,
|
||||
observeVisibleRects as observeVisibleRects_new,
|
||||
registerMutableSourceForHydration as registerMutableSourceForHydration_new,
|
||||
runWithPriority as runWithPriority_new,
|
||||
getCurrentUpdatePriority as getCurrentUpdatePriority_new,
|
||||
} from './ReactFiberReconciler.new';
|
||||
@@ -182,6 +184,9 @@ export const focusWithin = enableNewReconciler
|
||||
export const observeVisibleRects = enableNewReconciler
|
||||
? observeVisibleRects_new
|
||||
: observeVisibleRects_old;
|
||||
export const registerMutableSourceForHydration = enableNewReconciler
|
||||
? registerMutableSourceForHydration_new
|
||||
: registerMutableSourceForHydration_old;
|
||||
export const runWithPriority = enableNewReconciler
|
||||
? runWithPriority_new
|
||||
: runWithPriority_old;
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
} from './ReactFiberHotReloading.new';
|
||||
import {markRenderScheduled} from './SchedulingProfiler';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
export {registerMutableSourceForHydration} from './ReactMutableSource.new';
|
||||
export {createPortal} from './ReactPortal';
|
||||
export {
|
||||
createComponentSelector,
|
||||
|
||||
@@ -90,6 +90,7 @@ import {
|
||||
} from './ReactFiberHotReloading.old';
|
||||
import {markRenderScheduled} from './SchedulingProfiler';
|
||||
import ReactVersion from 'shared/ReactVersion';
|
||||
export {registerMutableSourceForHydration} from './ReactMutableSource.old';
|
||||
export {createPortal} from './ReactPortal';
|
||||
export {
|
||||
createComponentSelector,
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
|
||||
import type {RootTag} from './ReactRootTags';
|
||||
|
||||
import {noTimeout} from './ReactFiberHostConfig';
|
||||
import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
|
||||
import {createHostRootFiber} from './ReactFiber.new';
|
||||
import {
|
||||
NoLane,
|
||||
@@ -49,6 +49,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.suspendedLanes = NoLanes;
|
||||
this.pingedLanes = NoLanes;
|
||||
this.expiredLanes = NoLanes;
|
||||
this.mutableReadLanes = NoLanes;
|
||||
this.finishedLanes = NoLanes;
|
||||
|
||||
this.entangledLanes = NoLanes;
|
||||
@@ -59,6 +60,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.pooledCacheLanes = NoLanes;
|
||||
}
|
||||
|
||||
if (supportsHydration) {
|
||||
this.mutableSourceEagerHydrationData = null;
|
||||
}
|
||||
|
||||
if (enableSuspenseCallback) {
|
||||
this.hydrationCallbacks = null;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
import type {FiberRoot, SuspenseHydrationCallbacks} from './ReactInternalTypes';
|
||||
import type {RootTag} from './ReactRootTags';
|
||||
|
||||
import {noTimeout} from './ReactFiberHostConfig';
|
||||
import {noTimeout, supportsHydration} from './ReactFiberHostConfig';
|
||||
import {createHostRootFiber} from './ReactFiber.old';
|
||||
import {
|
||||
NoLane,
|
||||
@@ -49,6 +49,7 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.suspendedLanes = NoLanes;
|
||||
this.pingedLanes = NoLanes;
|
||||
this.expiredLanes = NoLanes;
|
||||
this.mutableReadLanes = NoLanes;
|
||||
this.finishedLanes = NoLanes;
|
||||
|
||||
this.entangledLanes = NoLanes;
|
||||
@@ -59,6 +60,10 @@ function FiberRootNode(containerInfo, tag, hydrate) {
|
||||
this.pooledCacheLanes = NoLanes;
|
||||
}
|
||||
|
||||
if (supportsHydration) {
|
||||
this.mutableSourceEagerHydrationData = null;
|
||||
}
|
||||
|
||||
if (enableSuspenseCallback) {
|
||||
this.hydrationCallbacks = null;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {Lanes} from './ReactFiberLane.new';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
|
||||
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
|
||||
|
||||
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
|
||||
import {
|
||||
ClassComponent,
|
||||
HostRoot,
|
||||
@@ -82,6 +83,7 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
popTopLevelLegacyContextObject(workInProgress);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
const flags = workInProgress.flags;
|
||||
invariant(
|
||||
(flags & DidCapture) === NoFlags,
|
||||
@@ -177,6 +179,7 @@ function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
|
||||
}
|
||||
popHostContainer(interruptedWork);
|
||||
popTopLevelLegacyContextObject(interruptedWork);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
break;
|
||||
}
|
||||
case HostComponent: {
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {Lanes} from './ReactFiberLane.old';
|
||||
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
|
||||
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
|
||||
|
||||
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
|
||||
import {
|
||||
ClassComponent,
|
||||
HostRoot,
|
||||
@@ -82,6 +83,7 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {
|
||||
}
|
||||
popHostContainer(workInProgress);
|
||||
popTopLevelLegacyContextObject(workInProgress);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
const flags = workInProgress.flags;
|
||||
invariant(
|
||||
(flags & DidCapture) === NoFlags,
|
||||
@@ -177,6 +179,7 @@ function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) {
|
||||
}
|
||||
popHostContainer(interruptedWork);
|
||||
popTopLevelLegacyContextObject(interruptedWork);
|
||||
resetMutableSourceWorkInProgressVersions();
|
||||
break;
|
||||
}
|
||||
case HostComponent: {
|
||||
|
||||
@@ -8,7 +8,14 @@
|
||||
*/
|
||||
|
||||
import type {Source} from 'shared/ReactElementType';
|
||||
import type {RefObject, ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
RefObject,
|
||||
ReactContext,
|
||||
MutableSourceSubscribeFn,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceVersion,
|
||||
MutableSource,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {SuspenseInstance} from './ReactFiberHostConfig';
|
||||
import type {WorkTag} from './ReactWorkTags';
|
||||
import type {TypeOfMode} from './ReactTypeOfMode';
|
||||
@@ -34,6 +41,7 @@ export type HookType =
|
||||
| 'useDebugValue'
|
||||
| 'useDeferredValue'
|
||||
| 'useTransition'
|
||||
| 'useMutableSource'
|
||||
| 'useSyncExternalStore'
|
||||
| 'useOpaqueIdentifier'
|
||||
| 'useCacheRefresh';
|
||||
@@ -206,6 +214,11 @@ type BaseFiberRootProperties = {|
|
||||
// Determines if we should attempt to hydrate on the initial mount
|
||||
+hydrate: boolean,
|
||||
|
||||
// Used by useMutableSource hook to avoid tearing during hydration.
|
||||
mutableSourceEagerHydrationData?: Array<
|
||||
MutableSource<any> | MutableSourceVersion,
|
||||
> | null,
|
||||
|
||||
// Node returned by Scheduler.scheduleCallback. Represents the next rendering
|
||||
// task that the root will work on.
|
||||
callbackNode: *,
|
||||
@@ -217,6 +230,7 @@ type BaseFiberRootProperties = {|
|
||||
suspendedLanes: Lanes,
|
||||
pingedLanes: Lanes,
|
||||
expiredLanes: Lanes,
|
||||
mutableReadLanes: Lanes,
|
||||
|
||||
finishedLanes: Lanes,
|
||||
|
||||
@@ -291,6 +305,11 @@ export type Dispatcher = {|
|
||||
useDebugValue<T>(value: T, formatterFn: ?(value: T) => mixed): void,
|
||||
useDeferredValue<T>(value: T): T,
|
||||
useTransition(): [boolean, (() => void) => void],
|
||||
useMutableSource<Source, Snapshot>(
|
||||
source: MutableSource<Source>,
|
||||
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
|
||||
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
|
||||
): Snapshot,
|
||||
useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
|
||||
108
packages/react-reconciler/src/ReactMutableSource.new.js
Normal file
108
packages/react-reconciler/src/ReactMutableSource.new.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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, MutableSourceVersion} from 'shared/ReactTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
|
||||
import {isPrimaryRenderer} from './ReactFiberHostConfig';
|
||||
|
||||
// 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.
|
||||
const workInProgressSources: Array<MutableSource<any>> = [];
|
||||
|
||||
let rendererSigil;
|
||||
if (__DEV__) {
|
||||
// Used to detect multiple renderers using the same mutable source.
|
||||
rendererSigil = {};
|
||||
}
|
||||
|
||||
export function markSourceAsDirty(mutableSource: MutableSource<any>): void {
|
||||
workInProgressSources.push(mutableSource);
|
||||
}
|
||||
|
||||
export function resetWorkInProgressVersions(): void {
|
||||
for (let i = 0; i < workInProgressSources.length; i++) {
|
||||
const mutableSource = workInProgressSources[i];
|
||||
if (isPrimaryRenderer) {
|
||||
mutableSource._workInProgressVersionPrimary = null;
|
||||
} else {
|
||||
mutableSource._workInProgressVersionSecondary = null;
|
||||
}
|
||||
}
|
||||
workInProgressSources.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;
|
||||
} else {
|
||||
mutableSource._workInProgressVersionSecondary = version;
|
||||
}
|
||||
workInProgressSources.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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eager reads the version of a mutable source and stores it on the root.
|
||||
// This ensures that the version used for server rendering matches the one
|
||||
// that is eventually read during hydration.
|
||||
// If they don't match there's a potential tear and a full deopt render is required.
|
||||
export function registerMutableSourceForHydration(
|
||||
root: FiberRoot,
|
||||
mutableSource: MutableSource<any>,
|
||||
): void {
|
||||
const getVersion = mutableSource._getVersion;
|
||||
const version = getVersion(mutableSource._source);
|
||||
|
||||
// TODO Clear this data once all pending hydration work is finished.
|
||||
// Retaining it forever may interfere with GC.
|
||||
if (root.mutableSourceEagerHydrationData == null) {
|
||||
root.mutableSourceEagerHydrationData = [mutableSource, version];
|
||||
} else {
|
||||
root.mutableSourceEagerHydrationData.push(mutableSource, version);
|
||||
}
|
||||
}
|
||||
108
packages/react-reconciler/src/ReactMutableSource.old.js
Normal file
108
packages/react-reconciler/src/ReactMutableSource.old.js
Normal file
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* 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, MutableSourceVersion} from 'shared/ReactTypes';
|
||||
import type {FiberRoot} from './ReactInternalTypes';
|
||||
|
||||
import {isPrimaryRenderer} from './ReactFiberHostConfig';
|
||||
|
||||
// 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.
|
||||
const workInProgressSources: Array<MutableSource<any>> = [];
|
||||
|
||||
let rendererSigil;
|
||||
if (__DEV__) {
|
||||
// Used to detect multiple renderers using the same mutable source.
|
||||
rendererSigil = {};
|
||||
}
|
||||
|
||||
export function markSourceAsDirty(mutableSource: MutableSource<any>): void {
|
||||
workInProgressSources.push(mutableSource);
|
||||
}
|
||||
|
||||
export function resetWorkInProgressVersions(): void {
|
||||
for (let i = 0; i < workInProgressSources.length; i++) {
|
||||
const mutableSource = workInProgressSources[i];
|
||||
if (isPrimaryRenderer) {
|
||||
mutableSource._workInProgressVersionPrimary = null;
|
||||
} else {
|
||||
mutableSource._workInProgressVersionSecondary = null;
|
||||
}
|
||||
}
|
||||
workInProgressSources.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;
|
||||
} else {
|
||||
mutableSource._workInProgressVersionSecondary = version;
|
||||
}
|
||||
workInProgressSources.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.',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Eager reads the version of a mutable source and stores it on the root.
|
||||
// This ensures that the version used for server rendering matches the one
|
||||
// that is eventually read during hydration.
|
||||
// If they don't match there's a potential tear and a full deopt render is required.
|
||||
export function registerMutableSourceForHydration(
|
||||
root: FiberRoot,
|
||||
mutableSource: MutableSource<any>,
|
||||
): void {
|
||||
const getVersion = mutableSource._getVersion;
|
||||
const version = getVersion(mutableSource._source);
|
||||
|
||||
// TODO Clear this data once all pending hydration work is finished.
|
||||
// Retaining it forever may interfere with GC.
|
||||
if (root.mutableSourceEagerHydrationData == null) {
|
||||
root.mutableSourceEagerHydrationData = [mutableSource, version];
|
||||
} else {
|
||||
root.mutableSourceEagerHydrationData.push(mutableSource, version);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
451
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
vendored
Normal file
451
packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js
vendored
Normal file
@@ -0,0 +1,451 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @emails react-core
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
let React;
|
||||
let ReactDOM;
|
||||
let ReactDOMServer;
|
||||
let Scheduler;
|
||||
let act;
|
||||
let createMutableSource;
|
||||
let useMutableSource;
|
||||
|
||||
describe('useMutableSourceHydration', () => {
|
||||
beforeEach(() => {
|
||||
jest.resetModules();
|
||||
|
||||
React = require('react');
|
||||
ReactDOM = require('react-dom');
|
||||
ReactDOMServer = require('react-dom/server');
|
||||
Scheduler = require('scheduler');
|
||||
|
||||
act = require('jest-react').act;
|
||||
|
||||
// Stable entrypoints export with "unstable_" prefix.
|
||||
createMutableSource =
|
||||
React.createMutableSource || React.unstable_createMutableSource;
|
||||
useMutableSource =
|
||||
React.useMutableSource || React.unstable_useMutableSource;
|
||||
});
|
||||
|
||||
function dispatchAndSetCurrentEvent(el, event) {
|
||||
try {
|
||||
window.event = event;
|
||||
el.dispatchEvent(event);
|
||||
} finally {
|
||||
window.event = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultGetSnapshot = source => source.value;
|
||||
const defaultSubscribe = (source, callback) => source.subscribe(callback);
|
||||
|
||||
function createComplexSource(initialValueA, initialValueB) {
|
||||
const callbacksA = [];
|
||||
const callbacksB = [];
|
||||
let revision = 0;
|
||||
let valueA = initialValueA;
|
||||
let valueB = initialValueB;
|
||||
|
||||
const subscribeHelper = (callbacks, callback) => {
|
||||
if (callbacks.indexOf(callback) < 0) {
|
||||
callbacks.push(callback);
|
||||
}
|
||||
return () => {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
subscribeA(callback) {
|
||||
return subscribeHelper(callbacksA, callback);
|
||||
},
|
||||
subscribeB(callback) {
|
||||
return subscribeHelper(callbacksB, callback);
|
||||
},
|
||||
|
||||
get listenerCountA() {
|
||||
return callbacksA.length;
|
||||
},
|
||||
get listenerCountB() {
|
||||
return callbacksB.length;
|
||||
},
|
||||
|
||||
set valueA(newValue) {
|
||||
revision++;
|
||||
valueA = newValue;
|
||||
callbacksA.forEach(callback => callback());
|
||||
},
|
||||
get valueA() {
|
||||
return valueA;
|
||||
},
|
||||
|
||||
set valueB(newValue) {
|
||||
revision++;
|
||||
valueB = newValue;
|
||||
callbacksB.forEach(callback => callback());
|
||||
},
|
||||
get valueB() {
|
||||
return valueB;
|
||||
},
|
||||
|
||||
get version() {
|
||||
return revision;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createSource(initialValue) {
|
||||
const callbacks = [];
|
||||
let revision = 0;
|
||||
let value = initialValue;
|
||||
return {
|
||||
subscribe(callback) {
|
||||
if (callbacks.indexOf(callback) < 0) {
|
||||
callbacks.push(callback);
|
||||
}
|
||||
return () => {
|
||||
const index = callbacks.indexOf(callback);
|
||||
if (index >= 0) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
};
|
||||
},
|
||||
get listenerCount() {
|
||||
return callbacks.length;
|
||||
},
|
||||
set value(newValue) {
|
||||
revision++;
|
||||
value = newValue;
|
||||
callbacks.forEach(callback => callback());
|
||||
},
|
||||
get value() {
|
||||
return value;
|
||||
},
|
||||
get version() {
|
||||
return revision;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function Component({getSnapshot, label, mutableSource, subscribe}) {
|
||||
const snapshot = useMutableSource(mutableSource, getSnapshot, subscribe);
|
||||
Scheduler.unstable_yieldValue(`${label}:${snapshot}`);
|
||||
return <div>{`${label}:${snapshot}`}</div>;
|
||||
}
|
||||
|
||||
it('should render and hydrate', () => {
|
||||
const source = createSource('one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<Component
|
||||
label="only"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
|
||||
container.innerHTML = htmlString;
|
||||
expect(Scheduler).toHaveYielded(['only:one']);
|
||||
expect(source.listenerCount).toBe(0);
|
||||
|
||||
const root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
mutableSources: [mutableSource],
|
||||
},
|
||||
});
|
||||
act(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
expect(Scheduler).toHaveYielded(['only:one']);
|
||||
expect(source.listenerCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should detect a tear before hydrating a component', () => {
|
||||
const source = createSource('one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<Component
|
||||
label="only"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
|
||||
container.innerHTML = htmlString;
|
||||
expect(Scheduler).toHaveYielded(['only:one']);
|
||||
expect(source.listenerCount).toBe(0);
|
||||
|
||||
const root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
mutableSources: [mutableSource],
|
||||
},
|
||||
});
|
||||
expect(() => {
|
||||
act(() => {
|
||||
root.render(<TestComponent />);
|
||||
|
||||
source.value = 'two';
|
||||
});
|
||||
}).toErrorDev(
|
||||
'Warning: An error occurred during hydration. ' +
|
||||
'The server HTML was replaced with client content in <div>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['only:two']);
|
||||
expect(source.listenerCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should detect a tear between hydrating components', () => {
|
||||
const source = createSource('one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
function TestComponent() {
|
||||
return (
|
||||
<>
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
<Component
|
||||
label="b"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const htmlString = ReactDOMServer.renderToString(<TestComponent />);
|
||||
container.innerHTML = htmlString;
|
||||
expect(Scheduler).toHaveYielded(['a:one', 'b:one']);
|
||||
expect(source.listenerCount).toBe(0);
|
||||
|
||||
const root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
mutableSources: [mutableSource],
|
||||
},
|
||||
});
|
||||
expect(() => {
|
||||
act(() => {
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.startTransition(() => {
|
||||
root.render(<TestComponent />);
|
||||
});
|
||||
} else {
|
||||
root.render(<TestComponent />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['a:one']);
|
||||
source.value = 'two';
|
||||
});
|
||||
}).toErrorDev(
|
||||
'Warning: An error occurred during hydration. ' +
|
||||
'The server HTML was replaced with client content in <div>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['a:two', 'b:two']);
|
||||
expect(source.listenerCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should detect a tear between hydrating components reading from different parts of a source', () => {
|
||||
const source = createComplexSource('a:one', 'b:one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
// Subscribe to part of the store.
|
||||
const getSnapshotA = s => s.valueA;
|
||||
const subscribeA = (s, callback) => s.subscribeA(callback);
|
||||
const getSnapshotB = s => s.valueB;
|
||||
const subscribeB = (s, callback) => s.subscribeB(callback);
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const htmlString = ReactDOMServer.renderToString(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
container.innerHTML = htmlString;
|
||||
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:one']);
|
||||
|
||||
const root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
mutableSources: [mutableSource],
|
||||
},
|
||||
});
|
||||
expect(() => {
|
||||
act(() => {
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.startTransition(() => {
|
||||
root.render(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
});
|
||||
} else {
|
||||
root.render(
|
||||
<>
|
||||
<Component
|
||||
label="0"
|
||||
getSnapshot={getSnapshotA}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeA}
|
||||
/>
|
||||
<Component
|
||||
label="1"
|
||||
getSnapshot={getSnapshotB}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={subscribeB}
|
||||
/>
|
||||
</>,
|
||||
);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough(['0:a:one']);
|
||||
source.valueB = 'b:two';
|
||||
});
|
||||
}).toErrorDev(
|
||||
'Warning: An error occurred during hydration. ' +
|
||||
'The server HTML was replaced with client content in <div>.',
|
||||
{withoutStack: true},
|
||||
);
|
||||
expect(Scheduler).toHaveYielded(['0:a:one', '1:b:two']);
|
||||
});
|
||||
|
||||
// @gate !enableSyncDefaultUpdates
|
||||
it('should detect a tear during a higher priority interruption', () => {
|
||||
const source = createSource('one');
|
||||
const mutableSource = createMutableSource(source, param => param.version);
|
||||
|
||||
function Unrelated({flag}) {
|
||||
Scheduler.unstable_yieldValue(flag);
|
||||
return flag;
|
||||
}
|
||||
|
||||
function TestComponent({flag}) {
|
||||
return (
|
||||
<>
|
||||
<Unrelated flag={flag} />
|
||||
<Component
|
||||
label="a"
|
||||
getSnapshot={defaultGetSnapshot}
|
||||
mutableSource={mutableSource}
|
||||
subscribe={defaultSubscribe}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const container = document.createElement('div');
|
||||
document.body.appendChild(container);
|
||||
|
||||
const htmlString = ReactDOMServer.renderToString(
|
||||
<TestComponent flag={1} />,
|
||||
);
|
||||
container.innerHTML = htmlString;
|
||||
expect(Scheduler).toHaveYielded([1, 'a:one']);
|
||||
expect(source.listenerCount).toBe(0);
|
||||
|
||||
const root = ReactDOM.createRoot(container, {
|
||||
hydrate: true,
|
||||
hydrationOptions: {
|
||||
mutableSources: [mutableSource],
|
||||
},
|
||||
});
|
||||
|
||||
expect(() => {
|
||||
act(() => {
|
||||
if (gate(flags => flags.enableSyncDefaultUpdates)) {
|
||||
React.startTransition(() => {
|
||||
root.render(<TestComponent flag={1} />);
|
||||
});
|
||||
} else {
|
||||
root.render(<TestComponent flag={1} />);
|
||||
}
|
||||
expect(Scheduler).toFlushAndYieldThrough([1]);
|
||||
|
||||
// Render an update which will be higher priority than the hydration.
|
||||
// We can do this by scheduling the update inside a mouseover event.
|
||||
const arbitraryElement = document.createElement('div');
|
||||
const mouseOverEvent = document.createEvent('MouseEvents');
|
||||
mouseOverEvent.initEvent('mouseover', true, true);
|
||||
arbitraryElement.addEventListener('mouseover', () => {
|
||||
root.render(<TestComponent flag={2} />);
|
||||
});
|
||||
dispatchAndSetCurrentEvent(arbitraryElement, mouseOverEvent);
|
||||
|
||||
expect(Scheduler).toFlushAndYieldThrough([2]);
|
||||
|
||||
source.value = 'two';
|
||||
});
|
||||
}).toErrorDev(
|
||||
[
|
||||
'Warning: An error occurred during hydration. ' +
|
||||
'The server HTML was replaced with client content in <div>.',
|
||||
|
||||
'Warning: Text content did not match. Server: "1" Client: "2"',
|
||||
],
|
||||
{withoutStack: 1},
|
||||
);
|
||||
expect(Scheduler).toHaveYielded([2, 'a:two']);
|
||||
expect(source.listenerCount).toBe(1);
|
||||
});
|
||||
});
|
||||
21
packages/react-server/src/ReactFizzHooks.js
vendored
21
packages/react-server/src/ReactFizzHooks.js
vendored
@@ -9,7 +9,12 @@
|
||||
|
||||
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
|
||||
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
|
||||
import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig';
|
||||
|
||||
@@ -444,6 +449,18 @@ export function useCallback<T>(
|
||||
return useMemo(() => callback, deps);
|
||||
}
|
||||
|
||||
// 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 useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
@@ -506,6 +523,8 @@ export const Dispatcher: DispatcherType = {
|
||||
useDeferredValue,
|
||||
useTransition,
|
||||
useOpaqueIdentifier,
|
||||
// Subscriptions are not setup in a server environment.
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
};
|
||||
|
||||
|
||||
@@ -827,6 +827,7 @@ const Dispatcher: DispatcherType = {
|
||||
useImperativeHandle: (unsupportedHook: any),
|
||||
useEffect: (unsupportedHook: any),
|
||||
useOpaqueIdentifier: (unsupportedHook: any),
|
||||
useMutableSource: (unsupportedHook: any),
|
||||
useSyncExternalStore: (unsupportedHook: any),
|
||||
useCacheRefresh(): <T>(?() => T, ?T) => void {
|
||||
return unsupportedRefresh;
|
||||
|
||||
@@ -44,6 +44,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
|
||||
useDeferredValue: unsupported,
|
||||
useTransition: unsupported,
|
||||
useOpaqueIdentifier: unsupported,
|
||||
useMutableSource: unsupported,
|
||||
useSyncExternalStore: unsupported,
|
||||
useCacheRefresh: unsupported,
|
||||
};
|
||||
|
||||
@@ -23,6 +23,8 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createFactory,
|
||||
createMutableSource,
|
||||
createMutableSource as unstable_createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -48,6 +50,8 @@ export {
|
||||
useLayoutEffect,
|
||||
unstable_useInsertionEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createFactory,
|
||||
createMutableSource as unstable_createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -44,6 +45,7 @@ export {
|
||||
unstable_useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
|
||||
@@ -46,6 +46,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createFactory,
|
||||
createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -69,6 +70,7 @@ export {
|
||||
unstable_useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
|
||||
@@ -22,6 +22,8 @@ export {
|
||||
cloneElement,
|
||||
createContext,
|
||||
createElement,
|
||||
createMutableSource,
|
||||
createMutableSource as unstable_createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -47,6 +49,8 @@ export {
|
||||
unstable_useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useSyncExternalStore as unstable_useSyncExternalStore,
|
||||
useReducer,
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
createContext,
|
||||
createElement,
|
||||
createFactory,
|
||||
createMutableSource as unstable_createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -37,6 +38,7 @@ export {
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useReducer,
|
||||
useRef,
|
||||
useState,
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
createFactoryWithValidation,
|
||||
cloneElementWithValidation,
|
||||
} from './ReactElementValidator';
|
||||
import {createMutableSource} from './ReactMutableSource';
|
||||
import ReactSharedInternals from './ReactSharedInternals';
|
||||
import {startTransition} from './ReactStartTransition';
|
||||
import {act} from './ReactAct';
|
||||
@@ -77,6 +79,7 @@ const Children = {
|
||||
|
||||
export {
|
||||
Children,
|
||||
createMutableSource,
|
||||
createRef,
|
||||
Component,
|
||||
PureComponent,
|
||||
@@ -92,6 +95,7 @@ export {
|
||||
useInsertionEffect as unstable_useInsertionEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useMutableSource,
|
||||
useSyncExternalStore,
|
||||
useReducer,
|
||||
useRef,
|
||||
|
||||
@@ -8,7 +8,12 @@
|
||||
*/
|
||||
|
||||
import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
|
||||
import type {ReactContext} from 'shared/ReactTypes';
|
||||
import type {
|
||||
MutableSource,
|
||||
MutableSourceGetSnapshotFn,
|
||||
MutableSourceSubscribeFn,
|
||||
ReactContext,
|
||||
} from 'shared/ReactTypes';
|
||||
import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig';
|
||||
|
||||
import ReactCurrentDispatcher from './ReactCurrentDispatcher';
|
||||
@@ -163,6 +168,15 @@ export function useOpaqueIdentifier(): OpaqueIDType | void {
|
||||
return dispatcher.useOpaqueIdentifier();
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export function useSyncExternalStore<T>(
|
||||
subscribe: (() => void) => () => void,
|
||||
getSnapshot: () => T,
|
||||
|
||||
34
packages/react/src/ReactMutableSource.js
Normal file
34
packages/react/src/ReactMutableSource.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* 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 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;
|
||||
|
||||
// Used to detect side effects that update a mutable source during render.
|
||||
// See https://github.com/facebook/react/issues/19948
|
||||
mutableSource._currentlyRenderingFiber = null;
|
||||
mutableSource._initialVersionAsOfFirstRender = null;
|
||||
}
|
||||
|
||||
return mutableSource;
|
||||
}
|
||||
@@ -17,6 +17,7 @@ export {
|
||||
SuspenseList,
|
||||
cloneElement,
|
||||
createElement,
|
||||
createMutableSource as unstable_createMutableSource,
|
||||
createRef,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
@@ -32,6 +33,7 @@ export {
|
||||
useDeferredValue,
|
||||
useDeferredValue as unstable_useDeferredValue,
|
||||
useMemo,
|
||||
useMutableSource as unstable_useMutableSource,
|
||||
useTransition,
|
||||
version,
|
||||
} from './src/React';
|
||||
|
||||
@@ -101,6 +101,56 @@ export type ReactScopeInstance = {|
|
||||
getChildContextValues: <T>(context: ReactContext<T>) => Array<T>,
|
||||
|};
|
||||
|
||||
// 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,
|
||||
|
||||
// DEV only
|
||||
// Used to detect side effects that update a mutable source during render.
|
||||
// See https://github.com/facebook/react/issues/19948
|
||||
_currentlyRenderingFiber?: Fiber | null,
|
||||
_initialVersionAsOfFirstRender?: MutableSourceVersion | null,
|
||||
|};
|
||||
|
||||
// The subset of a Thenable required by things thrown by Suspense.
|
||||
// This doesn't require a value to be passed to either handler.
|
||||
export interface Wakeable {
|
||||
|
||||
Reference in New Issue
Block a user