From 2bd76f8f36d76dffe2228db8eb1b195d4f695ba5 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 10 Sep 2021 15:02:07 -0400 Subject: [PATCH] Delete useMutableSource implementation This API was replaced by useSyncExternalStore --- .../react-debug-tools/src/ReactDebugHooks.js | 26 +- .../ReactHooksInspectionIntegration-test.js | 37 - packages/react-dom/src/client/ReactDOMRoot.js | 27 +- .../src/server/ReactPartialRendererHooks.js | 21 +- .../src/ReactFiberBeginWork.new.js | 18 - .../src/ReactFiberBeginWork.old.js | 18 - .../src/ReactFiberCompleteWork.new.js | 3 - .../src/ReactFiberCompleteWork.old.js | 3 - .../src/ReactFiberHooks.new.js | 401 +--- .../src/ReactFiberHooks.old.js | 401 +--- .../src/ReactFiberLane.new.js | 11 +- .../src/ReactFiberLane.old.js | 11 +- .../src/ReactFiberReconciler.js | 5 - .../src/ReactFiberReconciler.new.js | 1 - .../src/ReactFiberReconciler.old.js | 1 - .../src/ReactFiberRoot.new.js | 1 - .../src/ReactFiberRoot.old.js | 1 - .../src/ReactFiberUnwindWork.new.js | 3 - .../src/ReactFiberUnwindWork.old.js | 3 - .../src/ReactInternalTypes.js | 21 +- .../src/ReactMutableSource.new.js | 108 - .../src/ReactMutableSource.old.js | 108 - .../useMutableSource-test.internal.js | 2098 ----------------- .../useMutableSourceHydration-test.js | 451 ---- packages/react-server/src/ReactFizzHooks.js | 21 +- .../react-server/src/ReactFlightServer.js | 1 - .../src/ReactSuspenseTestUtils.js | 1 - packages/react/index.classic.fb.js | 4 - packages/react/index.experimental.js | 2 - packages/react/index.js | 2 - packages/react/index.modern.fb.js | 4 - packages/react/index.stable.js | 2 - packages/react/src/React.js | 4 - packages/react/src/ReactHooks.js | 16 +- packages/react/src/ReactMutableSource.js | 34 - .../unstable-shared-subset.experimental.js | 2 - packages/shared/ReactTypes.js | 50 - 37 files changed, 14 insertions(+), 3907 deletions(-) delete mode 100644 packages/react-reconciler/src/ReactMutableSource.new.js delete mode 100644 packages/react-reconciler/src/ReactMutableSource.old.js delete mode 100644 packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js delete mode 100644 packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js delete mode 100644 packages/react/src/ReactMutableSource.js diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index c1258c2dcda45..a57c38ab103c2 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -7,13 +7,7 @@ * @flow */ -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, - ReactProviderType, -} from 'shared/ReactTypes'; +import type {ReactContext, ReactProviderType} from 'shared/ReactTypes'; import type { Fiber, Dispatcher as DispatcherType, @@ -261,23 +255,6 @@ function useMemo( return value; } -function useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): 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( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -357,7 +334,6 @@ const Dispatcher: DispatcherType = { useRef, useState, useTransition, - useMutableSource, useSyncExternalStore, useDeferredValue, useOpaqueIdentifier, diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index b13bec22d213c..2e6896ef160fd 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1019,43 +1019,6 @@ 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
; - } - const renderer = ReactTestRenderer.create(); - 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; diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index ba062fc3f71e9..3bde63dc67a30 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -8,7 +8,7 @@ */ import type {Container} from './ReactDOMHostConfig'; -import type {MutableSource, ReactNodeList} from 'shared/ReactTypes'; +import type {ReactNodeList} from 'shared/ReactTypes'; import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes'; export type RootType = { @@ -24,7 +24,6 @@ export type CreateRootOptions = { hydrationOptions?: { onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, - mutableSources?: Array>, ... }, // END OF TODO @@ -35,7 +34,6 @@ export type CreateRootOptions = { export type HydrateRootOptions = { // Hydration options - hydratedSources?: Array>, onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, // Options for all roots @@ -61,7 +59,6 @@ import { createContainer, updateContainer, findHostInstanceWithNoPortals, - registerMutableSourceForHydration, } from 'react-reconciler/src/ReactFiberReconciler'; import invariant from 'shared/invariant'; import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags'; @@ -129,11 +126,6 @@ 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; @@ -159,15 +151,6 @@ 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); } @@ -185,7 +168,6 @@ 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; @@ -208,13 +190,6 @@ 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); diff --git a/packages/react-dom/src/server/ReactPartialRendererHooks.js b/packages/react-dom/src/server/ReactPartialRendererHooks.js index bfc21b0623a05..175a394a4dbf0 100644 --- a/packages/react-dom/src/server/ReactPartialRendererHooks.js +++ b/packages/react-dom/src/server/ReactPartialRendererHooks.js @@ -9,12 +9,7 @@ import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactContext} from 'shared/ReactTypes'; import type PartialRenderer from './ReactPartialRenderer'; import {validateContextBounds} from './ReactPartialRendererContext'; @@ -466,18 +461,6 @@ export function useCallback( 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: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - resolveCurrentlyRenderingComponent(); - return getSnapshot(source._source); -} - function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -536,8 +519,6 @@ export const Dispatcher: DispatcherType = { useDeferredValue, useTransition, useOpaqueIdentifier, - // Subscriptions are not setup in a server environment. - useMutableSource, useSyncExternalStore, }; diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.new.js b/packages/react-reconciler/src/ReactFiberBeginWork.new.js index d9874bba34119..894bc523e4b58 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.new.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.new.js @@ -12,7 +12,6 @@ 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, @@ -145,7 +144,6 @@ import { isSuspenseInstancePending, isSuspenseInstanceFallback, registerSuspenseInstanceRetry, - supportsHydration, isPrimaryRenderer, supportsPersistence, getOffscreenContainerProps, @@ -224,7 +222,6 @@ import { RetryAfterError, NoContext, } from './ReactFiberWorkLoop.new'; -import {setWorkInProgressVersion} from './ReactMutableSource.new'; import { requestCacheFromPool, pushCacheProvider, @@ -1303,21 +1300,6 @@ 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); - const version = mutableSourceEagerHydrationData[i + 1]; - setWorkInProgressVersion(mutableSource, version); - } - } - } - const child = mountChildFibers( workInProgress, null, diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.old.js b/packages/react-reconciler/src/ReactFiberBeginWork.old.js index 74037fbe966f2..264e723b28973 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.old.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.old.js @@ -12,7 +12,6 @@ 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, @@ -145,7 +144,6 @@ import { isSuspenseInstancePending, isSuspenseInstanceFallback, registerSuspenseInstanceRetry, - supportsHydration, isPrimaryRenderer, supportsPersistence, getOffscreenContainerProps, @@ -224,7 +222,6 @@ import { RetryAfterError, NoContext, } from './ReactFiberWorkLoop.old'; -import {setWorkInProgressVersion} from './ReactMutableSource.old'; import { requestCacheFromPool, pushCacheProvider, @@ -1303,21 +1300,6 @@ 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); - const version = mutableSourceEagerHydrationData[i + 1]; - setWorkInProgressVersion(mutableSource, version); - } - } - } - const child = mountChildFibers( workInProgress, null, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js index 1382439f1ee7a..e7d6f40e98bdb 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js @@ -30,8 +30,6 @@ 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 { @@ -854,7 +852,6 @@ function completeWork( } popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js index 746e3d4b572da..6e8406fc05dfd 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js @@ -30,8 +30,6 @@ 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 { @@ -854,7 +852,6 @@ function completeWork( } popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; diff --git a/packages/react-reconciler/src/ReactFiberHooks.new.js b/packages/react-reconciler/src/ReactFiberHooks.new.js index 4b4d7ad7e9161..5f1c30a834c15 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.new.js +++ b/packages/react-reconciler/src/ReactFiberHooks.new.js @@ -7,12 +7,7 @@ * @flow */ -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactContext} from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.new'; import type {HookFlags} from './ReactHookEffectTags'; @@ -50,7 +45,6 @@ import { intersectLanes, isTransitionLane, markRootEntangled, - markRootMutableRead, NoTimestamp, } from './ReactFiberLane.new'; import { @@ -102,12 +96,6 @@ 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'; @@ -118,7 +106,6 @@ import { entangleTransitions, } from './ReactUpdateQueue.new'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.new'; -import {getIsStrictModeForDevtools} from './ReactFiberReconciler.new'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -948,322 +935,6 @@ function rerenderReducer( return [newState, dispatch]; } -type MutableSourceMemoizedState = {| - refs: { - getSnapshot: MutableSourceGetSnapshotFn, - setSnapshot: Snapshot => void, - }, - source: MutableSource, - subscribe: MutableSourceSubscribeFn, -|}; - -function readFromUnsubscribedMutableSource( - root: FiberRoot, - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, -): 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. - // - // If it were to become visible to the user, it suggests one of two things: - // a bug in React, or (more likely), a mutation during the render phase that - // caused the second re-render attempt to be different from the first. - // - // We know it's the second case if the logs are currently disabled. So in - // dev, we can present a more accurate error message. - if (__DEV__) { - // eslint-disable-next-line react-internal/no-production-logging - if (getIsStrictModeForDevtools()) { - // If getIsStrictModeForDevtools is true, this is the dev-only double render - // This is only reachable if there was a mutation during render. Show a helpful - // error message. - // - // Something interesting to note: because we only double render in - // development, this error will never happen during production. This is - // actually true of all errors that occur during a double render, - // because if the first render had thrown, we would have exited the - // begin phase without double rendering. We should consider suppressing - // any error from a double render (with a warning) to more closely match - // the production behavior. - const componentName = getComponentNameFromFiber( - currentlyRenderingFiber, - ); - invariant( - false, - 'A mutable source was mutated while the %s component was rendering. ' + - 'This is not supported. Move any mutations into event handlers ' + - 'or effects.', - componentName, - ); - } - } - - // 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( - hook: Hook, - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): 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); - - // 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> = { - 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: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - const hook = mountWorkInProgressHook(); - hook.memoizedState = ({ - refs: { - getSnapshot, - setSnapshot: (null: any), - }, - source, - subscribe, - }: MutableSourceMemoizedState); - return useMutableSource(hook, source, getSnapshot, subscribe); -} - -function updateMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - const hook = updateWorkInProgressHook(); - return useMutableSource(hook, source, getSnapshot, subscribe); -} - function mountSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2334,7 +2005,6 @@ export const ContextOnlyDispatcher: Dispatcher = { useDebugValue: throwInvalidHookError, useDeferredValue: throwInvalidHookError, useTransition: throwInvalidHookError, - useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useOpaqueIdentifier: throwInvalidHookError, @@ -2361,7 +2031,6 @@ const HooksDispatcherOnMount: Dispatcher = { useDebugValue: mountDebugValue, useDeferredValue: mountDeferredValue, useTransition: mountTransition, - useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: mountOpaqueIdentifier, @@ -2388,7 +2057,6 @@ const HooksDispatcherOnUpdate: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: updateDeferredValue, useTransition: updateTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useOpaqueIdentifier: updateOpaqueIdentifier, @@ -2415,7 +2083,6 @@ const HooksDispatcherOnRerender: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: rerenderDeferredValue, useTransition: rerenderTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: rerenderOpaqueIdentifier, @@ -2565,15 +2232,6 @@ if (__DEV__) { mountHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - mountHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2705,15 +2363,6 @@ if (__DEV__) { updateHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2845,15 +2494,6 @@ if (__DEV__) { updateHookTypesDev(); return updateTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2986,15 +2626,6 @@ if (__DEV__) { updateHookTypesDev(); return rerenderTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3140,16 +2771,6 @@ if (__DEV__) { mountHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3297,16 +2918,6 @@ if (__DEV__) { updateHookTypesDev(); return updateTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3455,16 +3066,6 @@ if (__DEV__) { updateHookTypesDev(); return rerenderTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, diff --git a/packages/react-reconciler/src/ReactFiberHooks.old.js b/packages/react-reconciler/src/ReactFiberHooks.old.js index a03dcd6ca2db2..016b208d30d90 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.old.js +++ b/packages/react-reconciler/src/ReactFiberHooks.old.js @@ -7,12 +7,7 @@ * @flow */ -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactContext} from 'shared/ReactTypes'; import type {Fiber, Dispatcher, HookType} from './ReactInternalTypes'; import type {Lanes, Lane} from './ReactFiberLane.old'; import type {HookFlags} from './ReactHookEffectTags'; @@ -50,7 +45,6 @@ import { intersectLanes, isTransitionLane, markRootEntangled, - markRootMutableRead, NoTimestamp, } from './ReactFiberLane.old'; import { @@ -102,12 +96,6 @@ 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'; @@ -118,7 +106,6 @@ import { entangleTransitions, } from './ReactUpdateQueue.old'; import {pushInterleavedQueue} from './ReactFiberInterleavedUpdates.old'; -import {getIsStrictModeForDevtools} from './ReactFiberReconciler.old'; import {warnOnSubscriptionInsideStartTransition} from 'shared/ReactFeatureFlags'; const {ReactCurrentDispatcher, ReactCurrentBatchConfig} = ReactSharedInternals; @@ -948,322 +935,6 @@ function rerenderReducer( return [newState, dispatch]; } -type MutableSourceMemoizedState = {| - refs: { - getSnapshot: MutableSourceGetSnapshotFn, - setSnapshot: Snapshot => void, - }, - source: MutableSource, - subscribe: MutableSourceSubscribeFn, -|}; - -function readFromUnsubscribedMutableSource( - root: FiberRoot, - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, -): 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. - // - // If it were to become visible to the user, it suggests one of two things: - // a bug in React, or (more likely), a mutation during the render phase that - // caused the second re-render attempt to be different from the first. - // - // We know it's the second case if the logs are currently disabled. So in - // dev, we can present a more accurate error message. - if (__DEV__) { - // eslint-disable-next-line react-internal/no-production-logging - if (getIsStrictModeForDevtools()) { - // If getIsStrictModeForDevtools is true, this is the dev-only double render - // This is only reachable if there was a mutation during render. Show a helpful - // error message. - // - // Something interesting to note: because we only double render in - // development, this error will never happen during production. This is - // actually true of all errors that occur during a double render, - // because if the first render had thrown, we would have exited the - // begin phase without double rendering. We should consider suppressing - // any error from a double render (with a warning) to more closely match - // the production behavior. - const componentName = getComponentNameFromFiber( - currentlyRenderingFiber, - ); - invariant( - false, - 'A mutable source was mutated while the %s component was rendering. ' + - 'This is not supported. Move any mutations into event handlers ' + - 'or effects.', - componentName, - ); - } - } - - // 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( - hook: Hook, - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): 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); - - // 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> = { - 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: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - const hook = mountWorkInProgressHook(); - hook.memoizedState = ({ - refs: { - getSnapshot, - setSnapshot: (null: any), - }, - source, - subscribe, - }: MutableSourceMemoizedState); - return useMutableSource(hook, source, getSnapshot, subscribe); -} - -function updateMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - const hook = updateWorkInProgressHook(); - return useMutableSource(hook, source, getSnapshot, subscribe); -} - function mountSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2334,7 +2005,6 @@ export const ContextOnlyDispatcher: Dispatcher = { useDebugValue: throwInvalidHookError, useDeferredValue: throwInvalidHookError, useTransition: throwInvalidHookError, - useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useOpaqueIdentifier: throwInvalidHookError, @@ -2361,7 +2031,6 @@ const HooksDispatcherOnMount: Dispatcher = { useDebugValue: mountDebugValue, useDeferredValue: mountDeferredValue, useTransition: mountTransition, - useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: mountOpaqueIdentifier, @@ -2388,7 +2057,6 @@ const HooksDispatcherOnUpdate: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: updateDeferredValue, useTransition: updateTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useOpaqueIdentifier: updateOpaqueIdentifier, @@ -2415,7 +2083,6 @@ const HooksDispatcherOnRerender: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: rerenderDeferredValue, useTransition: rerenderTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: mountSyncExternalStore, useOpaqueIdentifier: rerenderOpaqueIdentifier, @@ -2565,15 +2232,6 @@ if (__DEV__) { mountHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - mountHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2705,15 +2363,6 @@ if (__DEV__) { updateHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2845,15 +2494,6 @@ if (__DEV__) { updateHookTypesDev(); return updateTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -2986,15 +2626,6 @@ if (__DEV__) { updateHookTypesDev(); return rerenderTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3140,16 +2771,6 @@ if (__DEV__) { mountHookTypesDev(); return mountTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - mountHookTypesDev(); - return mountMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3297,16 +2918,6 @@ if (__DEV__) { updateHookTypesDev(); return updateTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3455,16 +3066,6 @@ if (__DEV__) { updateHookTypesDev(); return rerenderTransition(); }, - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot { - currentHookNameInDev = 'useMutableSource'; - warnInvalidHookAccess(); - updateHookTypesDev(); - return updateMutableSource(source, getSnapshot, subscribe); - }, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, diff --git a/packages/react-reconciler/src/ReactFiberLane.new.js b/packages/react-reconciler/src/ReactFiberLane.new.js index ad124a432a4a2..15d8e7ff019d8 100644 --- a/packages/react-reconciler/src/ReactFiberLane.new.js +++ b/packages/react-reconciler/src/ReactFiberLane.new.js @@ -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, like - // useMutableSource, we should ensure that there is no partial work at the - // time we apply the entanglement. + // For those exceptions where entanglement is semantically important, 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,10 +617,6 @@ 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; @@ -631,7 +627,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.pingedLanes = 0; root.expiredLanes &= remainingLanes; - root.mutableReadLanes &= remainingLanes; root.entangledLanes &= remainingLanes; diff --git a/packages/react-reconciler/src/ReactFiberLane.old.js b/packages/react-reconciler/src/ReactFiberLane.old.js index 4a064a3846515..49927abb20bb7 100644 --- a/packages/react-reconciler/src/ReactFiberLane.old.js +++ b/packages/react-reconciler/src/ReactFiberLane.old.js @@ -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, like - // useMutableSource, we should ensure that there is no partial work at the - // time we apply the entanglement. + // For those exceptions where entanglement is semantically important, 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,10 +617,6 @@ 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; @@ -631,7 +627,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.pingedLanes = 0; root.expiredLanes &= remainingLanes; - root.mutableReadLanes &= remainingLanes; root.entangledLanes &= remainingLanes; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.js b/packages/react-reconciler/src/ReactFiberReconciler.js index 033b9eb0adbec..ae029cd01580b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -46,7 +46,6 @@ 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, getIsStrictModeForDevtools as getIsStrictModeForDevtools_old, @@ -85,7 +84,6 @@ 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, getIsStrictModeForDevtools as getIsStrictModeForDevtools_new, @@ -188,9 +186,6 @@ 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; diff --git a/packages/react-reconciler/src/ReactFiberReconciler.new.js b/packages/react-reconciler/src/ReactFiberReconciler.new.js index 06acfa11cbc3b..4219fdc72cf05 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.new.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.new.js @@ -93,7 +93,6 @@ import { } from './ReactFiberHotReloading.new'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.new'; export {createPortal} from './ReactPortal'; export { createComponentSelector, diff --git a/packages/react-reconciler/src/ReactFiberReconciler.old.js b/packages/react-reconciler/src/ReactFiberReconciler.old.js index 5a75a312c42b4..84bcb7e6e0d1a 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.old.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.old.js @@ -93,7 +93,6 @@ import { } from './ReactFiberHotReloading.old'; import {markRenderScheduled} from './SchedulingProfiler'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource.old'; export {createPortal} from './ReactPortal'; export { createComponentSelector, diff --git a/packages/react-reconciler/src/ReactFiberRoot.new.js b/packages/react-reconciler/src/ReactFiberRoot.new.js index 5e1f8275b694f..d69d2d77e5dfa 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.new.js +++ b/packages/react-reconciler/src/ReactFiberRoot.new.js @@ -49,7 +49,6 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.suspendedLanes = NoLanes; this.pingedLanes = NoLanes; this.expiredLanes = NoLanes; - this.mutableReadLanes = NoLanes; this.finishedLanes = NoLanes; this.entangledLanes = NoLanes; diff --git a/packages/react-reconciler/src/ReactFiberRoot.old.js b/packages/react-reconciler/src/ReactFiberRoot.old.js index 413a87fbcffc8..8469d6c85b506 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.old.js +++ b/packages/react-reconciler/src/ReactFiberRoot.old.js @@ -49,7 +49,6 @@ function FiberRootNode(containerInfo, tag, hydrate) { this.suspendedLanes = NoLanes; this.pingedLanes = NoLanes; this.expiredLanes = NoLanes; - this.mutableReadLanes = NoLanes; this.finishedLanes = NoLanes; this.entangledLanes = NoLanes; diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js index 152837286f5d2..cb0d90e1a7f5f 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.new.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.new.js @@ -13,7 +13,6 @@ 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, @@ -83,7 +82,6 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { } popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); const flags = workInProgress.flags; invariant( (flags & DidCapture) === NoFlags, @@ -179,7 +177,6 @@ function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) { } popHostContainer(interruptedWork); popTopLevelLegacyContextObject(interruptedWork); - resetMutableSourceWorkInProgressVersions(); break; } case HostComponent: { diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js index 88861db778be3..8fb63177c01ec 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.old.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.old.js @@ -13,7 +13,6 @@ 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, @@ -83,7 +82,6 @@ function unwindWork(workInProgress: Fiber, renderLanes: Lanes) { } popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); const flags = workInProgress.flags; invariant( (flags & DidCapture) === NoFlags, @@ -179,7 +177,6 @@ function unwindInterruptedWork(interruptedWork: Fiber, renderLanes: Lanes) { } popHostContainer(interruptedWork); popTopLevelLegacyContextObject(interruptedWork); - resetMutableSourceWorkInProgressVersions(); break; } case HostComponent: { diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index f07fd4634b3d6..692ec27bd7e80 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -8,14 +8,7 @@ */ import type {Source} from 'shared/ReactElementType'; -import type { - RefObject, - ReactContext, - MutableSourceSubscribeFn, - MutableSourceGetSnapshotFn, - MutableSourceVersion, - MutableSource, -} from 'shared/ReactTypes'; +import type {RefObject, ReactContext} from 'shared/ReactTypes'; import type {SuspenseInstance} from './ReactFiberHostConfig'; import type {WorkTag} from './ReactWorkTags'; import type {TypeOfMode} from './ReactTypeOfMode'; @@ -41,7 +34,6 @@ export type HookType = | 'useDebugValue' | 'useDeferredValue' | 'useTransition' - | 'useMutableSource' | 'useSyncExternalStore' | 'useOpaqueIdentifier' | 'useCacheRefresh'; @@ -214,11 +206,6 @@ 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 | MutableSourceVersion, - > | null, - // Node returned by Scheduler.scheduleCallback. Represents the next rendering // task that the root will work on. callbackNode: *, @@ -230,7 +217,6 @@ type BaseFiberRootProperties = {| suspendedLanes: Lanes, pingedLanes: Lanes, expiredLanes: Lanes, - mutableReadLanes: Lanes, finishedLanes: Lanes, @@ -305,11 +291,6 @@ export type Dispatcher = {| useDebugValue(value: T, formatterFn: ?(value: T) => mixed): void, useDeferredValue(value: T): T, useTransition(): [boolean, (() => void) => void], - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, diff --git a/packages/react-reconciler/src/ReactMutableSource.new.js b/packages/react-reconciler/src/ReactMutableSource.new.js deleted file mode 100644 index 61809d33f800d..0000000000000 --- a/packages/react-reconciler/src/ReactMutableSource.new.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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> = []; - -let rendererSigil; -if (__DEV__) { - // Used to detect multiple renderers using the same mutable source. - rendererSigil = {}; -} - -export function markSourceAsDirty(mutableSource: MutableSource): 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, -): null | MutableSourceVersion { - if (isPrimaryRenderer) { - return mutableSource._workInProgressVersionPrimary; - } else { - return mutableSource._workInProgressVersionSecondary; - } -} - -export function setWorkInProgressVersion( - mutableSource: MutableSource, - version: MutableSourceVersion, -): void { - if (isPrimaryRenderer) { - mutableSource._workInProgressVersionPrimary = version; - } else { - mutableSource._workInProgressVersionSecondary = version; - } - workInProgressSources.push(mutableSource); -} - -export function warnAboutMultipleRenderersDEV( - mutableSource: MutableSource, -): 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, -): 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); - } -} diff --git a/packages/react-reconciler/src/ReactMutableSource.old.js b/packages/react-reconciler/src/ReactMutableSource.old.js deleted file mode 100644 index 61809d33f800d..0000000000000 --- a/packages/react-reconciler/src/ReactMutableSource.old.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * 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> = []; - -let rendererSigil; -if (__DEV__) { - // Used to detect multiple renderers using the same mutable source. - rendererSigil = {}; -} - -export function markSourceAsDirty(mutableSource: MutableSource): 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, -): null | MutableSourceVersion { - if (isPrimaryRenderer) { - return mutableSource._workInProgressVersionPrimary; - } else { - return mutableSource._workInProgressVersionSecondary; - } -} - -export function setWorkInProgressVersion( - mutableSource: MutableSource, - version: MutableSourceVersion, -): void { - if (isPrimaryRenderer) { - mutableSource._workInProgressVersionPrimary = version; - } else { - mutableSource._workInProgressVersionSecondary = version; - } - workInProgressSources.push(mutableSource); -} - -export function warnAboutMultipleRenderersDEV( - mutableSource: MutableSource, -): 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, -): 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); - } -} diff --git a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js b/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js deleted file mode 100644 index c9988b4fc28aa..0000000000000 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ /dev/null @@ -1,2098 +0,0 @@ -/** - * 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 - * @jest-environment node - */ - -/* eslint-disable no-func-assign */ - -'use strict'; - -let React; -let ReactFeatureFlags; -let ReactNoop; -let Scheduler; -let act; -let createMutableSource; -let useMutableSource; - -function loadModules() { - jest.resetModules(); - jest.useFakeTimers(); - - ReactFeatureFlags = require('shared/ReactFeatureFlags'); - ReactFeatureFlags.enableProfilerTimer = true; - - React = require('react'); - ReactNoop = require('react-noop-renderer'); - 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; -} - -describe('useMutableSource', () => { - 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
{`${label}:${snapshot}`}
; - } - - beforeEach(loadModules); - - it('should subscribe to a source and schedule updates when it changes', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.renderToRootWithID( - <> - - - , - 'root', - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'a:one', - 'b:one', - 'Sync effect', - ]); - - // Subscriptions should be passive - expect(source.listenerCount).toBe(0); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(2); - - // Changing values should schedule an update with React - source.value = 'two'; - expect(Scheduler).toFlushAndYieldThrough(['a:two', 'b:two']); - - // Unmounting a component should remove its subscription. - ReactNoop.renderToRootWithID( - <> - - , - 'root', - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:two', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - - // Unmounting a root should remove the remaining event listeners - ReactNoop.unmountRootWithID('root'); - expect(Scheduler).toFlushAndYield([]); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(0); - - // Changes to source should not trigger an updates or warnings. - source.value = 'three'; - expect(Scheduler).toFlushAndYield([]); - }); - }); - - it('should restart work if a new source is mutated during render', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - } else { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - } - // Do enough work to read from one component - expect(Scheduler).toFlushAndYieldThrough(['a:one']); - - // Mutate source before continuing work - source.value = 'two'; - - // Render work should restart and the updated value should be used - expect(Scheduler).toFlushAndYield(['a:two', 'b:two', 'Sync effect']); - }); - }); - - it('should schedule an update if a new source is mutated between render and commit (subscription)', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - - // Finish rendering - expect(Scheduler).toFlushAndYieldThrough([ - 'a:one', - 'b:one', - 'Sync effect', - ]); - - // Mutate source before subscriptions are attached - expect(source.listenerCount).toBe(0); - source.value = 'two'; - - // Mutation should be detected, and a new render should be scheduled - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']); - }); - }); - - it('should unsubscribe and resubscribe if a new source is used', () => { - const sourceA = createSource('a-one'); - const mutableSourceA = createMutableSource( - sourceA, - param => param.versionA, - ); - - const sourceB = createSource('b-one'); - const mutableSourceB = createMutableSource( - sourceB, - param => param.versionB, - ); - - act(() => { - ReactNoop.render( - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(sourceA.listenerCount).toBe(1); - - // Changing values should schedule an update with React - sourceA.value = 'a-two'; - expect(Scheduler).toFlushAndYield(['only:a-two']); - - // If we re-render with a new source, the old one should be unsubscribed. - ReactNoop.render( - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:b-one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(sourceA.listenerCount).toBe(0); - expect(sourceB.listenerCount).toBe(1); - - // Changing to original source should not schedule updates with React - sourceA.value = 'a-three'; - expect(Scheduler).toFlushAndYield([]); - - // Changing new source value should schedule an update with React - sourceB.value = 'b-two'; - expect(Scheduler).toFlushAndYield(['only:b-two']); - }); - }); - - it('should unsubscribe and resubscribe if a new subscribe function is provided', () => { - const source = createSource('a-one'); - const mutableSource = createMutableSource(source, param => param.version); - - const unsubscribeA = jest.fn(); - const subscribeA = jest.fn(s => { - const unsubscribe = defaultSubscribe(s); - return () => { - unsubscribe(); - unsubscribeA(); - }; - }); - const unsubscribeB = jest.fn(); - const subscribeB = jest.fn(s => { - const unsubscribe = defaultSubscribe(s); - return () => { - unsubscribe(); - unsubscribeB(); - }; - }); - - act(() => { - ReactNoop.renderToRootWithID( - , - 'root', - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - expect(subscribeA).toHaveBeenCalledTimes(1); - - // If we re-render with a new subscription function, - // the old unsubscribe function should be called. - ReactNoop.renderToRootWithID( - , - 'root', - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:a-one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - expect(unsubscribeA).toHaveBeenCalledTimes(1); - expect(subscribeB).toHaveBeenCalledTimes(1); - - // Unmounting should call the newer unsubscribe. - ReactNoop.unmountRootWithID('root'); - expect(Scheduler).toFlushAndYield([]); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(0); - expect(unsubscribeB).toHaveBeenCalledTimes(1); - }); - }); - - it('should re-use previously read snapshot value when reading is unsafe', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']); - - // Changing values should schedule an update with React. - // Start working on this update but don't finish it. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - source.value = 'two'; - }); - } else { - source.value = 'two'; - } - expect(Scheduler).toFlushAndYieldThrough(['a:two']); - - // Re-renders that occur before the update is processed - // should reuse snapshot so long as the config has not changed - ReactNoop.flushSync(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - expect(Scheduler).toHaveYielded(['a:one', 'b:one', 'Sync effect']); - - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']); - }); - }); - - it('should read from source on newly mounted subtree if no pending updates are scheduled for source', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - <> - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'Sync effect']); - - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']); - }); - }); - - it('should throw and restart render if source and snapshot are unavailable during an update', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Changing values should schedule an update with React. - // Start working on this update but don't finish it. - ReactNoop.idleUpdates(() => { - source.value = 'two'; - expect(Scheduler).toFlushAndYieldThrough(['a:two']); - }); - - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s); - - // Force a higher priority render with a new config. - // This should signal that the snapshot is not safe and trigger a full re-render. - ReactNoop.flushSync(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - expect(Scheduler).toHaveYielded([ - 'a:new:two', - 'b:new:two', - 'Sync effect', - ]); - }); - }); - - it('should throw and restart render if source and snapshot are unavailable during a sync update', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Changing values should schedule an update with React. - // Start working on this update but don't finish it. - ReactNoop.idleUpdates(() => { - source.value = 'two'; - expect(Scheduler).toFlushAndYieldThrough(['a:two']); - }); - - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s); - - // Force a higher priority render with a new config. - // This should signal that the snapshot is not safe and trigger a full re-render. - ReactNoop.flushSync(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - expect(Scheduler).toHaveYielded([ - 'a:new:two', - 'b:new:two', - 'Sync effect', - ]); - }); - }); - - it('should only update components whose subscriptions fire', () => { - 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); - - act(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:a:one', 'b:b:one', 'Sync effect']); - - // Changes to part of the store (e.g. A) should not render other parts. - source.valueA = 'a:two'; - expect(Scheduler).toFlushAndYield(['a:a:two']); - source.valueB = 'b:two'; - expect(Scheduler).toFlushAndYield(['b:b:two']); - }); - }); - - it('should detect tearing in part of the store not yet subscribed to', () => { - 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); - - act(() => { - ReactNoop.render( - <> - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:a:one', 'Sync effect']); - - // Because the store has not changed yet, there are no pending updates, - // so it is considered safe to read from when we start this render. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render( - <> - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - } else { - ReactNoop.render( - <> - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - } - expect(Scheduler).toFlushAndYieldThrough(['a:a:one', 'b:b:one']); - - // Mutating the source should trigger a tear detection on the next read, - // which should throw and re-render the entire tree. - source.valueB = 'b:two'; - - expect(Scheduler).toFlushAndYield([ - 'a:a:one', - 'b:b:two', - 'c:b:two', - 'Sync effect', - ]); - }); - }); - - it('does not schedule an update for subscriptions that fire with an unchanged snapshot', () => { - const MockComponent = jest.fn(Component); - - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - ReactNoop.render( - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - - // Notify subscribe function but don't change the value - source.value = 'one'; - expect(Scheduler).toFlushWithoutYielding(); - }); - }); - - it('should throw and restart if getSnapshot changes between scheduled update and re-render', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s); - - let updateGetSnapshot; - - function WrapperWithState() { - const tuple = React.useState(() => defaultGetSnapshot); - updateGetSnapshot = tuple[1]; - return ( - - ); - } - - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Change the source (and schedule an update). - source.value = 'two'; - - // Schedule a higher priority update that changes getSnapshot. - ReactNoop.flushSync(() => { - updateGetSnapshot(() => newGetSnapshot); - }); - - expect(Scheduler).toHaveYielded(['only:new:two']); - }); - }); - - it('should recover from a mutation during yield when other work is scheduled', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - act(() => { - // Start a render that uses the mutable source. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - } else { - ReactNoop.render( - <> - - - , - ); - } - expect(Scheduler).toFlushAndYieldThrough(['a:one']); - - // Mutate source - source.value = 'two'; - - // Now render something different. - ReactNoop.render(
); - expect(Scheduler).toFlushAndYield([]); - }); - }); - - it('should not throw if the new getSnapshot returns the same snapshot value', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - const onRenderA = jest.fn(); - const onRenderB = jest.fn(); - - let updateGetSnapshot; - - function WrapperWithState() { - const tuple = React.useState(() => defaultGetSnapshot); - updateGetSnapshot = tuple[1]; - return ( - - ); - } - - act(() => { - ReactNoop.render( - <> - - - - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['a:one', 'b:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(onRenderA).toHaveBeenCalledTimes(1); - expect(onRenderB).toHaveBeenCalledTimes(1); - - // If B's getSnapshot function updates, but the snapshot it returns is the same, - // only B should re-render (to update its state). - updateGetSnapshot(() => s => defaultGetSnapshot(s)); - expect(Scheduler).toFlushAndYield(['b:one']); - ReactNoop.flushPassiveEffects(); - expect(onRenderA).toHaveBeenCalledTimes(1); - expect(onRenderB).toHaveBeenCalledTimes(2); - }); - }); - - it('should not throw if getSnapshot changes but the source can be safely read from anyway', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - const newGetSnapshot = s => 'new:' + defaultGetSnapshot(s); - - let updateGetSnapshot; - - function WrapperWithState() { - const tuple = React.useState(() => defaultGetSnapshot); - updateGetSnapshot = tuple[1]; - return ( - - ); - } - - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Change the source (and schedule an update) - // but also change the snapshot function too. - ReactNoop.batchedUpdates(() => { - source.value = 'two'; - updateGetSnapshot(() => newGetSnapshot); - }); - - expect(Scheduler).toFlushAndYield(['only:new:two']); - }); - }); - - it('should still schedule an update if an eager selector throws after a mutation', () => { - const source = createSource({ - friends: [ - {id: 1, name: 'Foo'}, - {id: 2, name: 'Bar'}, - ], - }); - const mutableSource = createMutableSource(source, param => param.version); - - function FriendsList() { - const getSnapshot = React.useCallback( - ({value}) => Array.from(value.friends), - [], - ); - const friends = useMutableSource( - mutableSource, - getSnapshot, - defaultSubscribe, - ); - return ( -
    - {friends.map(friend => ( - - ))} -
- ); - } - - function Friend({id}) { - const getSnapshot = React.useCallback( - ({value}) => { - // This selector is intentionally written in a way that will throw - // if no matching friend exists in the store. - return value.friends.find(friend => friend.id === id).name; - }, - [id], - ); - const name = useMutableSource( - mutableSource, - getSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue(`${id}:${name}`); - return
  • {name}
  • ; - } - - act(() => { - ReactNoop.render(, () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['1:Foo', '2:Bar', 'Sync effect']); - - // This mutation will cause the "Bar" component to throw, - // since its value will no longer be a part of the store. - // Mutable source should still schedule an update though, - // which should unmount "Bar" and mount "Baz". - source.value = { - friends: [ - {id: 1, name: 'Foo'}, - {id: 3, name: 'Baz'}, - ], - }; - expect(Scheduler).toFlushAndYield(['1:Foo', '3:Baz']); - }); - }); - - it('should not warn about updates that fire between unmount and passive unsubscribe', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - function Wrapper() { - React.useLayoutEffect(() => () => { - Scheduler.unstable_yieldValue('layout unmount'); - }); - return ( - - ); - } - - act(() => { - ReactNoop.renderToRootWithID(, 'root', () => - Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYield(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Unmounting a root should remove the remaining event listeners in a passive effect - ReactNoop.unmountRootWithID('root'); - expect(Scheduler).toFlushAndYieldThrough(['layout unmount']); - - // Changes to source should not cause a warning, - // even though the unsubscribe hasn't run yet (since it's a pending passive effect). - source.value = 'two'; - expect(Scheduler).toFlushAndYield([]); - }); - }); - - it('should support inline selectors and updates that are processed after selector change', async () => { - const source = createSource({ - a: 'initial', - b: 'initial', - }); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function mutateB(newB) { - source.value = { - ...source.value, - b: newB, - }; - } - - function App({getSnapshot}) { - const state = useMutableSource( - mutableSource, - getSnapshot, - defaultSubscribe, - ); - return state; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - expect(root).toMatchRenderedOutput('initial'); - - await act(async () => { - mutateB('Updated B'); - root.render(); - }); - expect(root).toMatchRenderedOutput('Updated B'); - - await act(async () => { - mutateB('Another update'); - }); - expect(root).toMatchRenderedOutput('Another update'); - }); - - it('should clear the update queue when getSnapshot changes with pending lower priority updates', async () => { - const source = createSource({ - a: 'initial', - b: 'initial', - }); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function mutateA(newA) { - source.value = { - ...source.value, - a: newA, - }; - } - - function mutateB(newB) { - source.value = { - ...source.value, - b: newB, - }; - } - - function App({toggle}) { - const state = useMutableSource( - mutableSource, - toggle ? getSnapshotB : getSnapshotA, - defaultSubscribe, - ); - const result = (toggle ? 'B: ' : 'A: ') + state; - return result; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - expect(root).toMatchRenderedOutput('A: initial'); - - await act(async () => { - ReactNoop.discreteUpdates(() => { - // Update both A and B to the same value - mutateA('Update'); - mutateB('Update'); - // Toggle to B in the same batch - root.render(); - }); - // Mutate A at lower priority. This should never be rendered, because - // by the time we get to the lower priority, we've already switched - // to B. - mutateA('OOPS! This mutation should be ignored'); - }); - expect(root).toMatchRenderedOutput('B: Update'); - }); - - it('should clear the update queue when source changes with pending lower priority updates', async () => { - const sourceA = createSource('initial'); - const sourceB = createSource('initial'); - const mutableSourceA = createMutableSource( - sourceA, - param => param.versionA, - ); - const mutableSourceB = createMutableSource( - sourceB, - param => param.versionB, - ); - - function App({toggle}) { - const state = useMutableSource( - toggle ? mutableSourceB : mutableSourceA, - defaultGetSnapshot, - defaultSubscribe, - ); - const result = (toggle ? 'B: ' : 'A: ') + state; - return result; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - expect(root).toMatchRenderedOutput('A: initial'); - - await act(async () => { - ReactNoop.discreteUpdates(() => { - // Update both A and B to the same value - sourceA.value = 'Update'; - sourceB.value = 'Update'; - // Toggle to B in the same batch - root.render(); - }); - // Mutate A at lower priority. This should never be rendered, because - // by the time we get to the lower priority, we've already switched - // to B. - sourceA.value = 'OOPS! This mutation should be ignored'; - }); - expect(root).toMatchRenderedOutput('B: Update'); - }); - - it('should always treat reading as potentially unsafe when getSnapshot changes between renders', async () => { - const source = createSource({ - a: 'foo', - b: 'bar', - }); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function mutateA(newA) { - source.value = { - ...source.value, - a: newA, - }; - } - - function App({getSnapshotFirst, getSnapshotSecond}) { - const first = useMutableSource( - mutableSource, - getSnapshotFirst, - defaultSubscribe, - ); - const second = useMutableSource( - mutableSource, - getSnapshotSecond, - defaultSubscribe, - ); - - let result = `x: ${first}, y: ${second}`; - - if (getSnapshotFirst === getSnapshotSecond) { - // When both getSnapshot functions are equal, - // the two values must be consistent. - if (first !== second) { - result = 'Oops, tearing!'; - } - } - - React.useEffect(() => { - Scheduler.unstable_yieldValue(result); - }, [result]); - - return result; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render( - , - ); - }); - // x and y start out reading from different parts of the store. - expect(Scheduler).toHaveYielded(['x: foo, y: bar']); - - await act(async () => { - ReactNoop.discreteUpdates(() => { - // At high priority, toggle y so that it reads from A instead of B. - // Simultaneously, mutate A. - mutateA('baz'); - root.render( - , - ); - - // If this update were processed before the next mutation, - // it would be expected to yield "baz" and "baz". - }); - - // At lower priority, mutate A again. - // This happens to match the initial value of B. - mutateA('bar'); - - // When this update is processed, - // it is expected to yield "bar" and "bar". - }); - - // Check that we didn't commit any inconsistent states. - // The actual sequence of work will be: - // 1. React renders the high-pri update, sees a new getSnapshot, detects the source has been further mutated, and throws - // 2. React re-renders with all pending updates, including the second mutation, and renders "bar" and "bar". - expect(Scheduler).toHaveYielded(['x: bar, y: bar']); - }); - - it('getSnapshot changes and then source is mutated in between paint and passive effect phase', async () => { - const source = createSource({ - a: 'foo', - b: 'bar', - }); - const mutableSource = createMutableSource(source, param => param.version); - - function mutateB(newB) { - source.value = { - ...source.value, - b: newB, - }; - } - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function App({getSnapshot}) { - const value = useMutableSource( - mutableSource, - getSnapshot, - defaultSubscribe, - ); - - Scheduler.unstable_yieldValue('Render: ' + value); - React.useEffect(() => { - Scheduler.unstable_yieldValue('Commit: ' + value); - }, [value]); - - return value; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - expect(Scheduler).toHaveYielded(['Render: foo', 'Commit: foo']); - - await act(async () => { - // Switch getSnapshot to read from B instead - root.render(); - // Render and finish the tree, but yield right after paint, before - // the passive effects have fired. - expect(Scheduler).toFlushUntilNextPaint(['Render: bar']); - // Then mutate B. - mutateB('baz'); - }); - expect(Scheduler).toHaveYielded([ - // Fires the effect from the previous render - 'Commit: bar', - // During that effect, it should detect that the snapshot has changed - // and re-render. - 'Render: baz', - 'Commit: baz', - ]); - expect(root).toMatchRenderedOutput('baz'); - }); - - it('getSnapshot changes and then source is mutated in between paint and passive effect phase, case 2', async () => { - const source = createSource({ - a: 'a0', - b: 'b0', - }); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function mutateA(newA) { - source.value = { - ...source.value, - a: newA, - }; - } - - function App({getSnapshotFirst, getSnapshotSecond}) { - const first = useMutableSource( - mutableSource, - getSnapshotFirst, - defaultSubscribe, - ); - const second = useMutableSource( - mutableSource, - getSnapshotSecond, - defaultSubscribe, - ); - - return `first: ${first}, second: ${second}`; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render( - , - ); - }); - expect(root.getChildrenAsJSX()).toEqual('first: a0, second: b0'); - - await act(async () => { - // Switch the second getSnapshot to also read from A - root.render( - , - ); - // Render and finish the tree, but yield right after paint, before - // the passive effects have fired. - expect(Scheduler).toFlushUntilNextPaint([]); - - // Now mutate A. Both hooks should update. - // This is at high priority so that it doesn't get batched with default - // priority updates that might fire during the passive effect - await act(async () => { - ReactNoop.discreteUpdates(() => { - mutateA('a1'); - }); - }); - - expect(root).toMatchRenderedOutput('first: a1, second: a1'); - }); - - expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1'); - }); - - it( - 'if source is mutated after initial read but before subscription is set ' + - 'up, should still entangle all pending mutations even if snapshot of ' + - 'new subscription happens to match', - async () => { - const source = createSource({ - a: 'a0', - b: 'b0', - }); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshotA = () => source.value.a; - const getSnapshotB = () => source.value.b; - - function mutateA(newA) { - source.value = { - ...source.value, - a: newA, - }; - } - - function mutateB(newB) { - source.value = { - ...source.value, - b: newB, - }; - } - - function Read({getSnapshot}) { - const value = useMutableSource( - mutableSource, - getSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue(value); - return value; - } - - function Text({text}) { - Scheduler.unstable_yieldValue(text); - return text; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render( - <> - - , - ); - }); - expect(Scheduler).toHaveYielded(['a0']); - expect(root).toMatchRenderedOutput('a0'); - - await act(async () => { - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - root.render( - <> - - - - , - ); - }); - } else { - root.render( - <> - - - - , - ); - } - - expect(Scheduler).toFlushAndYieldThrough(['a0', 'b0']); - // Mutate in an event. This schedules a subscription update on a, which - // already mounted, but not b, which hasn't subscribed yet. - mutateA('a1'); - mutateB('b1'); - - // Mutate again at lower priority. This will schedule another subscription - // update on a, but not b. When b mounts and subscriptions, the value it - // read during render will happen to match the latest value. But it should - // still entangle the updates to prevent the previous update (a1) from - // rendering by itself. - React.startTransition(() => { - mutateA('a0'); - mutateB('b0'); - }); - // Finish the current render - expect(Scheduler).toFlushUntilNextPaint(['c']); - // a0 will re-render because of the mutation update. But it should show - // the latest value, not the intermediate one, to avoid tearing with b. - expect(Scheduler).toFlushUntilNextPaint(['a0']); - - expect(root).toMatchRenderedOutput('a0b0c'); - // We should be done. - expect(Scheduler).toFlushAndYield([]); - expect(root).toMatchRenderedOutput('a0b0c'); - }); - }, - ); - - it('warns about functions being used as snapshot values', async () => { - const source = createSource(() => 'a'); - const mutableSource = createMutableSource(source, param => param.version); - - const getSnapshot = () => source.value; - - function Read() { - const fn = useMutableSource(mutableSource, getSnapshot, defaultSubscribe); - const value = fn(); - Scheduler.unstable_yieldValue(value); - return value; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render( - <> - - , - ); - expect(() => expect(Scheduler).toFlushAndYield(['a'])).toErrorDev( - 'Mutable source should not return a function as the snapshot value.', - ); - }); - expect(root).toMatchRenderedOutput('a'); - }); - - it('getSnapshot changes and then source is mutated during interleaved event', async () => { - const {useEffect} = React; - - const source = createComplexSource('1', '2'); - 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 configA = [getSnapshotA, subscribeA]; - - const getSnapshotB = s => s.valueB; - const subscribeB = (s, callback) => s.subscribeB(callback); - const configB = [getSnapshotB, subscribeB]; - - function App({parentConfig, childConfig}) { - const [getSnapshot, subscribe] = parentConfig; - const parentValue = useMutableSource( - mutableSource, - getSnapshot, - subscribe, - ); - - Scheduler.unstable_yieldValue('Parent: ' + parentValue); - - return ( - - ); - } - - function Child({parentConfig, childConfig, parentValue}) { - const [getSnapshot, subscribe] = childConfig; - const childValue = useMutableSource( - mutableSource, - getSnapshot, - subscribe, - ); - - Scheduler.unstable_yieldValue('Child: ' + childValue); - - let result = `${parentValue}, ${childValue}`; - - if (parentConfig === childConfig) { - // When both components read using the same config, the two values - // must be consistent. - if (parentValue !== childValue) { - result = 'Oops, tearing!'; - } - } - - useEffect(() => { - Scheduler.unstable_yieldValue('Commit: ' + result); - }, [result]); - - return result; - } - - const root = ReactNoop.createRoot(); - await act(async () => { - root.render(); - }); - expect(Scheduler).toHaveYielded(['Parent: 1', 'Child: 2', 'Commit: 1, 2']); - - await act(async () => { - // Switch the parent and the child to read using the same config - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - root.render(); - }); - } else { - root.render(); - } - // Start rendering the parent, but yield before rendering the child - expect(Scheduler).toFlushAndYieldThrough(['Parent: 2']); - - // Mutate the config. This is at lower priority so that 1) to make sure - // it doesn't happen to get batched with the in-progress render, and 2) - // so it doesn't interrupt the in-progress render. - React.startTransition(() => { - source.valueB = '3'; - }); - - if (gate(flags => flags.enableSyncDefaultUpdates)) { - // In default sync mode, all of the updates flush sync. - expect(Scheduler).toFlushAndYieldThrough([ - // The partial render completes - 'Child: 2', - 'Commit: 2, 2', - 'Parent: 3', - 'Child: 3', - ]); - - expect(Scheduler).toFlushAndYield([ - // Now finish the rest of the update - 'Commit: 3, 3', - ]); - } else { - expect(Scheduler).toFlushAndYieldThrough([ - // The partial render completes - 'Child: 2', - 'Commit: 2, 2', - ]); - - // Now there are two pending mutations at different priorities. But they - // both read the same version of the mutable source, so we must render - // them simultaneously. - // - expect(Scheduler).toFlushAndYieldThrough([ - 'Parent: 3', - // Demonstrates that we can yield here - ]); - expect(Scheduler).toFlushAndYield([ - // Now finish the rest of the update - 'Child: 3', - 'Commit: 3, 3', - ]); - } - }); - }); - - it('should not tear with newly mounted component when updates were scheduled at a lower priority', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - let committedA = null; - let committedB = null; - - const onRender = () => { - if (committedB !== null) { - expect(committedA).toBe(committedB); - } - }; - - function ComponentA() { - const snapshot = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue(`a:${snapshot}`); - React.useEffect(() => { - committedA = snapshot; - }, [snapshot]); - return
    {`a:${snapshot}`}
    ; - } - function ComponentB() { - const snapshot = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue(`b:${snapshot}`); - React.useEffect(() => { - committedB = snapshot; - }, [snapshot]); - return
    {`b:${snapshot}`}
    ; - } - - // Mount ComponentA with data version 1 - act(() => { - ReactNoop.render( - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - }); - expect(Scheduler).toHaveYielded(['a:one', 'Sync effect']); - expect(source.listenerCount).toBe(1); - - // Mount ComponentB with version 1 (but don't commit it) - act(() => { - ReactNoop.render( - - - - , - () => Scheduler.unstable_yieldValue('Sync effect'), - ); - expect(Scheduler).toFlushAndYieldThrough([ - 'a:one', - 'b:one', - 'Sync effect', - ]); - expect(source.listenerCount).toBe(1); - - // Mutate -> schedule update for ComponentA - React.startTransition(() => { - source.value = 'two'; - }); - - // Commit ComponentB -> notice the change and schedule an update for ComponentB - expect(Scheduler).toFlushAndYield(['a:two', 'b:two']); - expect(source.listenerCount).toBe(2); - }); - }); - - if (__DEV__) { - // See https://github.com/facebook/react/issues/19948 - describe('side effects detection', () => { - it('should throw if a mutable source is mutated during render', () => { - const source = createSource(0); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - let mutatedValueInRender = 1; - function MutateDuringRead() { - const value = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue('MutateDuringRead:' + value); - // Note that mutating an external value during render is a side effect and is not supported. - source.value = mutatedValueInRender++; - return null; - } - - // TODO The mechanism for this type of detection relies on StrictMode double rendering. - expect(() => { - act(() => { - ReactNoop.render( - - - , - ); - }); - }).toThrow( - 'A mutable source was mutated while the MutateDuringRead component ' + - 'was rendering. This is not supported. Move any mutations into ' + - 'event handlers or effects.', - ); - - expect(Scheduler).toHaveYielded([ - // First attempt - 'MutateDuringRead:0', - - // Synchronous retry - 'MutateDuringRead:1', - ]); - }); - - it('should throw if a mutable source is mutated during render (legacy mode)', () => { - const source = createSource('initial'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - function MutateDuringRead() { - const value = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue('MutateDuringRead:' + value); - // Note that mutating an external value during render is a side effect and is not supported. - if (value === 'initial') { - source.value = 'updated'; - } - return null; - } - - expect(() => { - act(() => { - ReactNoop.renderLegacySyncRoot( - - - , - ); - }); - }).toThrow( - 'A mutable source was mutated while the MutateDuringRead component ' + - 'was rendering. This is not supported. Move any mutations into ' + - 'event handlers or effects.', - ); - - expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']); - }); - - it('should not misidentify mutations after render as side effects', async () => { - const source = createSource('initial'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - function MutateDuringRead() { - const value = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.unstable_yieldValue('MutateDuringRead:' + value); - return null; - } - - await act(async () => { - ReactNoop.renderLegacySyncRoot( - - - , - ); - }); - expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']); - - await act(async () => { - source.value = 'updated'; - }); - expect(Scheduler).toHaveYielded(['MutateDuringRead:updated']); - }); - }); - - describe('dev warnings', () => { - it('should warn if the subscribe function does not return an unsubscribe function', () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - const brokenSubscribe = () => {}; - - expect(() => { - act(() => { - ReactNoop.render( - , - ); - }); - }).toErrorDev( - 'Mutable source subscribe function must return an unsubscribe function.', - ); - }); - - it('should error if multiple renderers of the same type use a mutable source at the same time', () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - act(() => { - // Start a render that uses the mutable source. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - } else { - ReactNoop.render( - <> - - - , - ); - } - expect(Scheduler).toFlushAndYieldThrough(['a:one']); - - const PrevScheduler = Scheduler; - - // Get a new copy of ReactNoop. - loadModules(); - - spyOnDev(console, 'error'); - - // Use the mutablesource again but with a different renderer. - ReactNoop.render( - , - ); - expect(Scheduler).toFlushAndYieldThrough(['c:one']); - - expect(console.error.calls.argsFor(0)[0]).toContain( - 'Detected multiple renderers concurrently rendering the ' + - 'same mutable source. This is currently unsupported.', - ); - - // TODO (useMutableSource) Act will automatically flush remaining work from render 1, - // but at this point something in the hooks dispatcher has been broken by jest.resetModules() - // Figure out what this is and remove this catch. - expect(() => - PrevScheduler.unstable_flushAllWithoutAsserting(), - ).toThrow('Invalid hook call'); - }); - }); - - it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - act(() => { - // Start a render that uses the mutable source. - if (gate(flags => flags.enableSyncDefaultUpdates)) { - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - } else { - ReactNoop.render( - <> - - - , - ); - } - expect(Scheduler).toFlushAndYieldThrough(['a:one']); - - const PrevScheduler = Scheduler; - - // Get a new copy of ReactNoop. - loadModules(); - - spyOnDev(console, 'error'); - - // Mutate before the new render reads from the source. - source.value = 'two'; - - // Use the mutablesource again but with a different renderer. - ReactNoop.render( - , - ); - expect(Scheduler).toFlushAndYieldThrough(['c:two']); - - expect(console.error.calls.argsFor(0)[0]).toContain( - 'Detected multiple renderers concurrently rendering the ' + - 'same mutable source. This is currently unsupported.', - ); - - // TODO (useMutableSource) Act will automatically flush remaining work from render 1, - // but at this point something in the hooks dispatcher has been broken by jest.resetModules() - // Figure out what this is and remove this catch. - expect(() => - PrevScheduler.unstable_flushAllWithoutAsserting(), - ).toThrow('Invalid hook call'); - }); - }); - }); - } -}); diff --git a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js b/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js deleted file mode 100644 index 7f46d1cb00552..0000000000000 --- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js +++ /dev/null @@ -1,451 +0,0 @@ -/** - * 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
    {`${label}:${snapshot}`}
    ; - } - - it('should render and hydrate', () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - function TestComponent() { - return ( - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - const htmlString = ReactDOMServer.renderToString(); - 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(); - }); - 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 ( - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - const htmlString = ReactDOMServer.renderToString(); - 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(); - - source.value = 'two'; - }); - }).toErrorDev( - 'Warning: An error occurred during hydration. ' + - 'The server HTML was replaced with client content in
    .', - {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 ( - <> - - - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - const htmlString = ReactDOMServer.renderToString(); - 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(); - }); - } else { - root.render(); - } - expect(Scheduler).toFlushAndYieldThrough(['a:one']); - source.value = 'two'; - }); - }).toErrorDev( - 'Warning: An error occurred during hydration. ' + - 'The server HTML was replaced with client content in
    .', - {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( - <> - - - , - ); - 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( - <> - - - , - ); - }); - } else { - root.render( - <> - - - , - ); - } - 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
    .', - {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 ( - <> - - - - ); - } - - const container = document.createElement('div'); - document.body.appendChild(container); - - const htmlString = ReactDOMServer.renderToString( - , - ); - 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(); - }); - } else { - root.render(); - } - 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(); - }); - 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
    .', - - 'Warning: Text content did not match. Server: "1" Client: "2"', - ], - {withoutStack: 1}, - ); - expect(Scheduler).toHaveYielded([2, 'a:two']); - expect(source.listenerCount).toBe(1); - }); -}); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 1618537ac1bd7..c272ddbbb9ad2 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -9,12 +9,7 @@ import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes'; -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactContext} from 'shared/ReactTypes'; import type {ResponseState, OpaqueIDType} from './ReactServerFormatConfig'; @@ -449,18 +444,6 @@ export function useCallback( 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: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - resolveCurrentlyRenderingComponent(); - return getSnapshot(source._source); -} - function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -515,8 +498,6 @@ export const Dispatcher: DispatcherType = { useDeferredValue, useTransition, useOpaqueIdentifier, - // Subscriptions are not setup in a server environment. - useMutableSource, useSyncExternalStore, }; diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 5429d3a3b9e60..a555f7cac8c68 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -827,7 +827,6 @@ const Dispatcher: DispatcherType = { useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), useOpaqueIdentifier: (unsupportedHook: any), - useMutableSource: (unsupportedHook: any), useSyncExternalStore: (unsupportedHook: any), useCacheRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; diff --git a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js index b4b57c68b5f56..e4c886dc85aa6 100644 --- a/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js +++ b/packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js @@ -44,7 +44,6 @@ export function waitForSuspense(fn: () => T): Promise { useDeferredValue: unsupported, useTransition: unsupported, useOpaqueIdentifier: unsupported, - useMutableSource: unsupported, useSyncExternalStore: unsupported, useCacheRefresh: unsupported, }; diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 7f8abc863157a..d3ebdb5c02fb8 100644 --- a/packages/react/index.classic.fb.js +++ b/packages/react/index.classic.fb.js @@ -23,8 +23,6 @@ export { createContext, createElement, createFactory, - createMutableSource, - createMutableSource as unstable_createMutableSource, createRef, forwardRef, isValidElement, @@ -50,8 +48,6 @@ export { useLayoutEffect, unstable_useInsertionEffect, useMemo, - useMutableSource, - useMutableSource as unstable_useMutableSource, useSyncExternalStore, useSyncExternalStore as unstable_useSyncExternalStore, useReducer, diff --git a/packages/react/index.experimental.js b/packages/react/index.experimental.js index a0a9bbaca216e..7ff629fd0af03 100644 --- a/packages/react/index.experimental.js +++ b/packages/react/index.experimental.js @@ -22,7 +22,6 @@ export { createContext, createElement, createFactory, - createMutableSource as unstable_createMutableSource, createRef, forwardRef, isValidElement, @@ -45,7 +44,6 @@ export { unstable_useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource as unstable_useMutableSource, useSyncExternalStore as unstable_useSyncExternalStore, useReducer, useRef, diff --git a/packages/react/index.js b/packages/react/index.js index 1b4552a90f5d8..17916a322bfa9 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -46,7 +46,6 @@ export { createContext, createElement, createFactory, - createMutableSource, createRef, forwardRef, isValidElement, @@ -70,7 +69,6 @@ export { unstable_useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, useSyncExternalStore, useSyncExternalStore as unstable_useSyncExternalStore, useReducer, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index 819d5fe90afd3..e2a55bbdab87f 100644 --- a/packages/react/index.modern.fb.js +++ b/packages/react/index.modern.fb.js @@ -22,8 +22,6 @@ export { cloneElement, createContext, createElement, - createMutableSource, - createMutableSource as unstable_createMutableSource, createRef, forwardRef, isValidElement, @@ -49,8 +47,6 @@ export { unstable_useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, - useMutableSource as unstable_useMutableSource, useSyncExternalStore, useSyncExternalStore as unstable_useSyncExternalStore, useReducer, diff --git a/packages/react/index.stable.js b/packages/react/index.stable.js index 008875e4577e5..70ede828c8a4e 100644 --- a/packages/react/index.stable.js +++ b/packages/react/index.stable.js @@ -22,7 +22,6 @@ export { createContext, createElement, createFactory, - createMutableSource as unstable_createMutableSource, createRef, forwardRef, isValidElement, @@ -38,7 +37,6 @@ export { useImperativeHandle, useLayoutEffect, useMemo, - useMutableSource as unstable_useMutableSource, useReducer, useRef, useState, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 541e7a35d3d3e..a171d16b8af80 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -44,7 +44,6 @@ import { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, useSyncExternalStore, useReducer, useRef, @@ -59,7 +58,6 @@ import { createFactoryWithValidation, cloneElementWithValidation, } from './ReactElementValidator'; -import {createMutableSource} from './ReactMutableSource'; import ReactSharedInternals from './ReactSharedInternals'; import {startTransition} from './ReactStartTransition'; import {act} from './ReactAct'; @@ -79,7 +77,6 @@ const Children = { export { Children, - createMutableSource, createRef, Component, PureComponent, @@ -95,7 +92,6 @@ export { useInsertionEffect as unstable_useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, useSyncExternalStore, useReducer, useRef, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 0722cf4b209b7..77dbd53c9ce19 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -8,12 +8,7 @@ */ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; -import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, - ReactContext, -} from 'shared/ReactTypes'; +import type {ReactContext} from 'shared/ReactTypes'; import type {OpaqueIDType} from 'react-reconciler/src/ReactFiberHostConfig'; import ReactCurrentDispatcher from './ReactCurrentDispatcher'; @@ -168,15 +163,6 @@ export function useOpaqueIdentifier(): OpaqueIDType | void { return dispatcher.useOpaqueIdentifier(); } -export function useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, -): Snapshot { - const dispatcher = resolveDispatcher(); - return dispatcher.useMutableSource(source, getSnapshot, subscribe); -} - export function useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, diff --git a/packages/react/src/ReactMutableSource.js b/packages/react/src/ReactMutableSource.js deleted file mode 100644 index f8e5d0b283037..0000000000000 --- a/packages/react/src/ReactMutableSource.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 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: Source, - getVersion: MutableSourceGetVersionFn, -): MutableSource { - const mutableSource: MutableSource = { - _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; -} diff --git a/packages/react/unstable-shared-subset.experimental.js b/packages/react/unstable-shared-subset.experimental.js index 9381778b4435d..c18138226ad3e 100644 --- a/packages/react/unstable-shared-subset.experimental.js +++ b/packages/react/unstable-shared-subset.experimental.js @@ -17,7 +17,6 @@ export { SuspenseList, cloneElement, createElement, - createMutableSource as unstable_createMutableSource, createRef, forwardRef, isValidElement, @@ -33,7 +32,6 @@ export { useDeferredValue, useDeferredValue as unstable_useDeferredValue, useMemo, - useMutableSource as unstable_useMutableSource, useTransition, version, } from './src/React'; diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 43f42bddb91d3..277f633387d11 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -101,56 +101,6 @@ export type ReactScopeInstance = {| getChildContextValues: (context: ReactContext) => Array, |}; -// 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; - -export type MutableSourceGetSnapshotFn< - Source: $NonMaybeType, - Snapshot, -> = (source: Source) => Snapshot; - -export type MutableSourceSubscribeFn, Snapshot> = ( - source: Source, - callback: (snapshot: Snapshot) => void, -) => () => void; - -export type MutableSourceGetVersionFn = ( - source: $NonMaybeType, -) => MutableSourceVersion; - -export type MutableSource> = {| - _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 {