diff --git a/packages/react-debug-tools/src/ReactDebugHooks.js b/packages/react-debug-tools/src/ReactDebugHooks.js index 42fc7fbe1628..6c8acd1e2f92 100644 --- a/packages/react-debug-tools/src/ReactDebugHooks.js +++ b/packages/react-debug-tools/src/ReactDebugHooks.js @@ -8,9 +8,6 @@ */ import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, ReactContext, ReactProviderType, StartTransitionOptions, @@ -273,23 +270,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, @@ -396,7 +376,6 @@ const Dispatcher: DispatcherType = { useRef, useState, useTransition, - useMutableSource, useSyncExternalStore, useDeferredValue, useId, diff --git a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js index 7b27b57f63ad..7283f3af7216 100644 --- a/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js +++ b/packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js @@ -1036,52 +1036,6 @@ describe('ReactHooksInspectionIntegration', () => { ]); }); - // @gate enableUseMutableSource - 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', []); - React.useMemo(() => 'not used', []); - 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: [], - }, - { - id: 2, - isStateEditable: false, - name: 'Memo', - value: 'not used', - subHooks: [], - }, - ]); - }); - it('should support composite useSyncExternalStore hook', () => { const useSyncExternalStore = React.useSyncExternalStore; function Foo() { diff --git a/packages/react-dom/src/client/ReactDOMRoot.js b/packages/react-dom/src/client/ReactDOMRoot.js index 2167175a90b1..b55b09eb2124 100644 --- a/packages/react-dom/src/client/ReactDOMRoot.js +++ b/packages/react-dom/src/client/ReactDOMRoot.js @@ -7,7 +7,7 @@ * @flow */ -import type {MutableSource, ReactNodeList} from 'shared/ReactTypes'; +import type {ReactNodeList} from 'shared/ReactTypes'; import type { FiberRoot, TransitionTracingCallbacks, @@ -47,7 +47,6 @@ export type CreateRootOptions = { export type HydrateRootOptions = { // Hydration options - hydratedSources?: Array>, onHydrated?: (suspenseNode: Comment) => void, onDeleted?: (suspenseNode: Comment) => void, // Options for all roots @@ -77,7 +76,6 @@ import { createHydrationContainer, updateContainer, findHostInstanceWithNoPortals, - registerMutableSourceForHydration, flushSync, isAlreadyRendering, } from 'react-reconciler/src/ReactFiberReconciler'; @@ -298,8 +296,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; - // TODO: Delete this option - const mutableSources = (options != null && options.hydratedSources) || null; let isStrictMode = false; let concurrentUpdatesByDefaultOverride = false; @@ -344,13 +340,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); - } - } - // $FlowFixMe[invalid-constructor] Flow no longer supports calling new on functions return new ReactDOMHydrationRoot(root); } diff --git a/packages/react-reconciler/src/ReactFiberBeginWork.js b/packages/react-reconciler/src/ReactFiberBeginWork.js index f237a26f9070..f0690a3d8d43 100644 --- a/packages/react-reconciler/src/ReactFiberBeginWork.js +++ b/packages/react-reconciler/src/ReactFiberBeginWork.js @@ -11,7 +11,6 @@ import type { ReactProviderType, ReactContext, ReactNodeList, - MutableSource, } from 'shared/ReactTypes'; import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy'; import type {Fiber, FiberRoot} from './ReactInternalTypes'; @@ -106,7 +105,6 @@ import { enableTransitionTracing, enableLegacyHidden, enableCPUSuspense, - enableUseMutableSource, enableFloat, enableHostSingletons, enableFormActions, @@ -261,7 +259,6 @@ import { getWorkInProgressRoot, } from './ReactFiberWorkLoop'; import {enqueueConcurrentRenderForLane} from './ReactFiberConcurrentUpdates'; -import {setWorkInProgressVersion} from './ReactMutableSource'; import {pushCacheProvider, CacheContext} from './ReactFiberCacheComponent'; import { createCapturedValue, @@ -1532,19 +1529,6 @@ function updateHostRoot( } else { // The outermost shell has not hydrated yet. Start hydrating. enterHydrationState(workInProgress); - if (enableUseMutableSource) { - 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, diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.js b/packages/react-reconciler/src/ReactFiberCompleteWork.js index 63255e9e6ad7..7071b211fa4e 100644 --- a/packages/react-reconciler/src/ReactFiberCompleteWork.js +++ b/packages/react-reconciler/src/ReactFiberCompleteWork.js @@ -41,8 +41,6 @@ import { diffInCommitPhase, } from 'shared/ReactFeatureFlags'; -import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource'; - import {now} from './Scheduler'; import { @@ -1038,7 +1036,6 @@ function completeWork( popRootTransition(workInProgress, fiberRoot, renderLanes); popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); if (fiberRoot.pendingContext) { fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index f40380d8cdb7..1b842a205c49 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -8,9 +8,6 @@ */ import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, ReactContext, StartTransitionOptions, Usable, @@ -37,7 +34,6 @@ import { enableCache, enableUseRefAccessWarning, enableLazyContextPropagation, - enableUseMutableSource, enableTransitionTracing, enableUseMemoCacheHook, enableUseEffectEventHook, @@ -74,7 +70,6 @@ import { intersectLanes, isTransitionLane, markRootEntangled, - markRootMutableRead, } from './ReactFiberLane'; import { ContinuousEventPriority, @@ -117,12 +112,6 @@ import { checkIfWorkInProgressReceivedUpdate, } from './ReactFiberBeginWork'; import {getIsHydrating} from './ReactFiberHydrationContext'; -import { - getWorkInProgressVersion, - markSourceAsDirty, - setWorkInProgressVersion, - warnAboutMultipleRenderersDEV, -} from './ReactMutableSource'; import {logStateUpdateScheduled} from './DebugTracing'; import { markStateUpdateScheduled, @@ -1403,301 +1392,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. - - // We expect this error not to be thrown during the synchronous retry, - // because we blocked interleaved mutations. - throw new Error( - '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 { - if (!enableUseMutableSource) { - return (undefined: any); - } - - const root = ((getWorkInProgressRoot(): any): FiberRoot); - - if (root === null) { - throw new Error( - '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, - lanes: NoLanes, - dispatch: null, - lastRenderedReducer: basicStateReducer, - lastRenderedState: snapshot, - }; - newQueue.dispatch = setSnapshot = (dispatchSetState.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 { - if (!enableUseMutableSource) { - return (undefined: any); - } - - 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 { - if (!enableUseMutableSource) { - return (undefined: any); - } - - const hook = updateWorkInProgressHook(); - return useMutableSource(hook, source, getSnapshot, subscribe); -} - function mountSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, @@ -3203,7 +2897,6 @@ export const ContextOnlyDispatcher: Dispatcher = { useDebugValue: throwInvalidHookError, useDeferredValue: throwInvalidHookError, useTransition: throwInvalidHookError, - useMutableSource: throwInvalidHookError, useSyncExternalStore: throwInvalidHookError, useId: throwInvalidHookError, }; @@ -3241,7 +2934,6 @@ const HooksDispatcherOnMount: Dispatcher = { useDebugValue: mountDebugValue, useDeferredValue: mountDeferredValue, useTransition: mountTransition, - useMutableSource: mountMutableSource, useSyncExternalStore: mountSyncExternalStore, useId: mountId, }; @@ -3279,7 +2971,6 @@ const HooksDispatcherOnUpdate: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: updateDeferredValue, useTransition: updateTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useId: updateId, }; @@ -3317,7 +3008,6 @@ const HooksDispatcherOnRerender: Dispatcher = { useDebugValue: updateDebugValue, useDeferredValue: rerenderDeferredValue, useTransition: rerenderTransition, - useMutableSource: updateMutableSource, useSyncExternalStore: updateSyncExternalStore, useId: updateId, }; @@ -3478,15 +3168,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, @@ -3646,15 +3327,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, @@ -3818,15 +3490,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, @@ -3989,15 +3652,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, @@ -4174,16 +3828,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, @@ -4371,16 +4015,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, @@ -4568,16 +4202,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.js b/packages/react-reconciler/src/ReactFiberLane.js index 835bf0119342..e90b02e44195 100644 --- a/packages/react-reconciler/src/ReactFiberLane.js +++ b/packages/react-reconciler/src/ReactFiberLane.js @@ -299,8 +299,8 @@ 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 + // 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) { @@ -622,10 +622,6 @@ export function markRootPinged(root: FiberRoot, pingedLanes: Lanes) { 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; @@ -636,7 +632,6 @@ export function markRootFinished(root: FiberRoot, remainingLanes: Lanes) { root.pingedLanes = NoLanes; 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 505349f94d1e..4b5193585a0b 100644 --- a/packages/react-reconciler/src/ReactFiberReconciler.js +++ b/packages/react-reconciler/src/ReactFiberReconciler.js @@ -97,7 +97,6 @@ import { findHostInstancesForRefresh, } from './ReactFiberHotReloading'; import ReactVersion from 'shared/ReactVersion'; -export {registerMutableSourceForHydration} from './ReactMutableSource'; export {createPortal} from './ReactPortal'; export { createComponentSelector, diff --git a/packages/react-reconciler/src/ReactFiberRoot.js b/packages/react-reconciler/src/ReactFiberRoot.js index 7fd4802d0cc7..7b295bdbe80b 100644 --- a/packages/react-reconciler/src/ReactFiberRoot.js +++ b/packages/react-reconciler/src/ReactFiberRoot.js @@ -17,7 +17,7 @@ import type {RootTag} from './ReactRootTags'; import type {Cache} from './ReactFiberCacheComponent'; import type {Container} from './ReactFiberConfig'; -import {noTimeout, supportsHydration} from './ReactFiberConfig'; +import {noTimeout} from './ReactFiberConfig'; import {createHostRootFiber} from './ReactFiber'; import { NoLane, @@ -72,7 +72,6 @@ function FiberRootNode( this.suspendedLanes = NoLanes; this.pingedLanes = NoLanes; this.expiredLanes = NoLanes; - this.mutableReadLanes = NoLanes; this.finishedLanes = NoLanes; this.errorRecoveryDisabledLanes = NoLanes; @@ -89,10 +88,6 @@ function FiberRootNode( this.pooledCacheLanes = NoLanes; } - if (supportsHydration) { - this.mutableSourceEagerHydrationData = null; - } - if (enableSuspenseCallback) { this.hydrationCallbacks = null; } diff --git a/packages/react-reconciler/src/ReactFiberUnwindWork.js b/packages/react-reconciler/src/ReactFiberUnwindWork.js index 592596e5ec76..fdbd3357ed3c 100644 --- a/packages/react-reconciler/src/ReactFiberUnwindWork.js +++ b/packages/react-reconciler/src/ReactFiberUnwindWork.js @@ -14,7 +14,6 @@ import type {SuspenseState} from './ReactFiberSuspenseComponent'; import type {Cache} from './ReactFiberCacheComponent'; import type {TracingMarkerInstance} from './ReactFiberTracingMarkerComponent'; -import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource'; import { ClassComponent, HostRoot, @@ -103,7 +102,6 @@ function unwindWork( popRootTransition(workInProgress, root, renderLanes); popHostContainer(workInProgress); popTopLevelLegacyContextObject(workInProgress); - resetMutableSourceWorkInProgressVersions(); const flags = workInProgress.flags; if ( (flags & ShouldCapture) !== NoFlags && @@ -234,7 +232,6 @@ function unwindInterruptedWork( popRootTransition(interruptedWork, root, renderLanes); popHostContainer(interruptedWork); popTopLevelLegacyContextObject(interruptedWork); - resetMutableSourceWorkInProgressVersions(); break; } case HostHoistable: diff --git a/packages/react-reconciler/src/ReactInternalTypes.js b/packages/react-reconciler/src/ReactInternalTypes.js index fd08c6f85c79..eae140ac4ed7 100644 --- a/packages/react-reconciler/src/ReactInternalTypes.js +++ b/packages/react-reconciler/src/ReactInternalTypes.js @@ -11,10 +11,6 @@ import type {Source} from 'shared/ReactElementType'; import type { RefObject, ReactContext, - MutableSourceSubscribeFn, - MutableSourceGetSnapshotFn, - MutableSourceVersion, - MutableSource, StartTransitionOptions, Wakeable, Usable, @@ -54,7 +50,6 @@ export type HookType = | 'useDebugValue' | 'useDeferredValue' | 'useTransition' - | 'useMutableSource' | 'useSyncExternalStore' | 'useId' | 'useCacheRefresh' @@ -237,11 +232,6 @@ type BaseFiberRootProperties = { context: Object | null, pendingContext: Object | null, - // Used by useMutableSource hook to avoid tearing during hydration. - mutableSourceEagerHydrationData?: Array< - MutableSource | MutableSourceVersion, - > | null, - // Used to create a linked list that represent all the roots that have // pending work scheduled on them. next: FiberRoot | null, @@ -257,7 +247,6 @@ type BaseFiberRootProperties = { suspendedLanes: Lanes, pingedLanes: Lanes, expiredLanes: Lanes, - mutableReadLanes: Lanes, errorRecoveryDisabledLanes: Lanes, finishedLanes: Lanes, @@ -410,11 +399,6 @@ export type Dispatcher = { boolean, (callback: () => void, options?: StartTransitionOptions) => void, ], - useMutableSource( - source: MutableSource, - getSnapshot: MutableSourceGetSnapshotFn, - subscribe: MutableSourceSubscribeFn, - ): Snapshot, useSyncExternalStore( subscribe: (() => void) => () => void, getSnapshot: () => T, diff --git a/packages/react-reconciler/src/ReactMutableSource.js b/packages/react-reconciler/src/ReactMutableSource.js deleted file mode 100644 index b5f1bd197b78..000000000000 --- a/packages/react-reconciler/src/ReactMutableSource.js +++ /dev/null @@ -1,108 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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 './ReactFiberConfig'; - -// 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 73a4c557ee1e..000000000000 --- a/packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js +++ /dev/null @@ -1,1886 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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; -let waitFor; -let waitForAll; -let assertLog; -let waitForPaint; - -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('internal-test-utils').act; - - const InternalTestUtils = require('internal-test-utils'); - waitFor = InternalTestUtils.waitFor; - waitForAll = InternalTestUtils.waitForAll; - waitForPaint = InternalTestUtils.waitForPaint; - assertLog = InternalTestUtils.assertLog; - - // 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.log(`${label}:${snapshot}`); - return
{`${label}:${snapshot}`}
; - } - - beforeEach(loadModules); - - // @gate enableUseMutableSource - it('should subscribe to a source and schedule updates when it changes', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.renderToRootWithID( - <> - - - , - 'root', - () => Scheduler.log('Sync effect'), - ); - await waitFor(['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'; - await waitFor(['a:two', 'b:two']); - - // Unmounting a component should remove its subscription. - ReactNoop.renderToRootWithID( - <> - - , - 'root', - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:two', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - - // Unmounting a root should remove the remaining event listeners - ReactNoop.unmountRootWithID('root'); - await waitForAll([]); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(0); - - // Changes to source should not trigger an updates or warnings. - source.value = 'three'; - await waitForAll([]); - }); - }); - - // @gate enableUseMutableSource - it('should restart work if a new source is mutated during render', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - React.startTransition(() => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - }); - - // Do enough work to read from one component - await waitFor(['a:one']); - - // Mutate source before continuing work - source.value = 'two'; - - // Render work should restart and the updated value should be used - await waitForAll(['a:two', 'b:two', 'Sync effect']); - }); - }); - - // @gate enableUseMutableSource - it('should schedule an update if a new source is mutated between render and commit (subscription)', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - - // Finish rendering - await waitFor(['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 - await waitForAll(['a:two', 'b:two']); - }); - }); - - // @gate enableUseMutableSource - it('should unsubscribe and resubscribe if a new source is used', async () => { - const sourceA = createSource('a-one'); - const mutableSourceA = createMutableSource( - sourceA, - param => param.versionA, - ); - - const sourceB = createSource('b-one'); - const mutableSourceB = createMutableSource( - sourceB, - param => param.versionB, - ); - - await act(async () => { - ReactNoop.render( - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['only:a-one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(sourceA.listenerCount).toBe(1); - - // Changing values should schedule an update with React - sourceA.value = 'a-two'; - await waitForAll(['only:a-two']); - - // If we re-render with a new source, the old one should be unsubscribed. - ReactNoop.render( - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['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'; - await waitForAll([]); - - // Changing new source value should schedule an update with React - sourceB.value = 'b-two'; - await waitForAll(['only:b-two']); - }); - }); - - // @gate enableUseMutableSource - it('should unsubscribe and resubscribe if a new subscribe function is provided', async () => { - 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(); - }; - }); - - await act(async () => { - ReactNoop.renderToRootWithID( - , - 'root', - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['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.log('Sync effect'), - ); - await waitForAll(['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'); - await waitForAll([]); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(0); - expect(unsubscribeB).toHaveBeenCalledTimes(1); - }); - }); - - // @gate enableUseMutableSource - it('should re-use previously read snapshot value when reading is unsafe', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:one', 'b:one', 'Sync effect']); - - // Changing values should schedule an update with React. - // Start working on this update but don't finish it. - React.startTransition(() => { - source.value = 'two'; - }); - await waitFor(['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.log('Sync effect'), - ); - }); - assertLog(['a:one', 'b:one', 'Sync effect']); - - await waitForAll(['a:two', 'b:two']); - }); - }); - - // @gate enableUseMutableSource - it('should read from source on newly mounted subtree if no pending updates are scheduled for source', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - <> - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:one', 'Sync effect']); - - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:one', 'b:one', 'Sync effect']); - }); - }); - - // @gate enableUseMutableSource - it('should throw and restart render if source and snapshot are unavailable during an update', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:one', 'b:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Changing values should schedule an update with React. - ReactNoop.idleUpdates(() => { - source.value = 'two'; - }); - - // Start working on this update but don't finish it. - await waitFor(['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.log('Sync effect'), - ); - }); - assertLog(['a:new:two', 'b:new:two', 'Sync effect']); - }); - }); - - // @gate enableUseMutableSource - it('should throw and restart render if source and snapshot are unavailable during a sync update', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['a:one', 'b:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Changing values should schedule an update with React. - ReactNoop.idleUpdates(() => { - source.value = 'two'; - }); - - // Start working on this update but don't finish it. - await waitFor(['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.log('Sync effect'), - ); - }); - assertLog(['a:new:two', 'b:new:two', 'Sync effect']); - }); - }); - - // @gate enableUseMutableSource - it('should only update components whose subscriptions fire', async () => { - 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); - - await act(async () => { - ReactNoop.render( - <> - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['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'; - await waitForAll(['a:a:two']); - source.valueB = 'b:two'; - await waitForAll(['b:b:two']); - }); - }); - - // @gate enableUseMutableSource - it('should detect tearing in part of the store not yet subscribed to', async () => { - 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); - - await act(async () => { - ReactNoop.render( - <> - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['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. - React.startTransition(() => { - ReactNoop.render( - <> - - - - , - () => Scheduler.log('Sync effect'), - ); - }); - - await waitFor(['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'; - - await waitForAll(['a:a:one', 'b:b:two', 'c:b:two', 'Sync effect']); - }); - }); - - // @gate enableUseMutableSource - it('does not schedule an update for subscriptions that fire with an unchanged snapshot', async () => { - const MockComponent = jest.fn(Component); - - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - ReactNoop.render( - , - () => Scheduler.log('Sync effect'), - ); - await waitFor(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - expect(source.listenerCount).toBe(1); - - // Notify subscribe function but don't change the value - source.value = 'one'; - await waitForAll([]); - }); - }); - - // @gate enableUseMutableSource - it('should throw and restart if getSnapshot changes between scheduled update and re-render', async () => { - 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 ( - - ); - } - - await act(async () => { - ReactNoop.render(, () => - Scheduler.log('Sync effect'), - ); - await waitForAll(['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); - }); - - assertLog(['only:new:two']); - }); - }); - - // @gate enableUseMutableSource - it('should recover from a mutation during yield when other work is scheduled', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - await act(async () => { - // Start a render that uses the mutable source. - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - - await waitFor(['a:one']); - - // Mutate source - source.value = 'two'; - - // Now render something different. - ReactNoop.render(
); - await waitForAll([]); - }); - }); - - // @gate enableUseMutableSource - it('should not throw if the new getSnapshot returns the same snapshot value', async () => { - 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 ( - - ); - } - - await act(async () => { - ReactNoop.render( - <> - - - - - - - , - () => Scheduler.log('Sync effect'), - ); - await waitForAll(['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)); - await waitForAll(['b:one']); - ReactNoop.flushPassiveEffects(); - expect(onRenderA).toHaveBeenCalledTimes(1); - expect(onRenderB).toHaveBeenCalledTimes(2); - }); - }); - - // @gate enableUseMutableSource - it('should not throw if getSnapshot changes but the source can be safely read from anyway', async () => { - 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 ( - - ); - } - - await act(async () => { - ReactNoop.render(, () => - Scheduler.log('Sync effect'), - ); - await waitForAll(['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); - }); - - await waitForAll(['only:new:two']); - }); - }); - - // @gate enableUseMutableSource - it('should still schedule an update if an eager selector throws after a mutation', async () => { - 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.log(`${id}:${name}`); - return
  • {name}
  • ; - } - - await act(async () => { - ReactNoop.render(, () => Scheduler.log('Sync effect')); - await waitForAll(['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'}, - ], - }; - await waitForAll(['1:Foo', '3:Baz']); - }); - }); - - // @gate enableUseMutableSource - it('should not warn about updates that fire between unmount and passive unsubscribe', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource(source, param => param.version); - - function Wrapper() { - React.useLayoutEffect(() => () => { - Scheduler.log('layout unmount'); - }); - return ( - - ); - } - - await act(async () => { - ReactNoop.renderToRootWithID(, 'root', () => - Scheduler.log('Sync effect'), - ); - await waitForAll(['only:one', 'Sync effect']); - ReactNoop.flushPassiveEffects(); - - // Unmounting a root should remove the remaining event listeners in a passive effect - ReactNoop.unmountRootWithID('root'); - await waitFor(['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'; - await waitForAll([]); - }); - }); - - // @gate enableUseMutableSource - 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(() => { - root.render(); - }); - expect(root).toMatchRenderedOutput('initial'); - - await act(() => { - mutateB('Updated B'); - root.render(); - }); - expect(root).toMatchRenderedOutput('Updated B'); - - await act(() => { - mutateB('Another update'); - }); - expect(root).toMatchRenderedOutput('Another update'); - }); - - // @gate enableUseMutableSource - 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(() => { - root.render(); - }); - expect(root).toMatchRenderedOutput('A: initial'); - - await act(() => { - 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'); - }); - - // @gate enableUseMutableSource - 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(() => { - root.render(); - }); - expect(root).toMatchRenderedOutput('A: initial'); - - await act(() => { - 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'); - }); - - // @gate enableUseMutableSource - 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.log(result); - }, [result]); - - return result; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - , - ); - }); - // x and y start out reading from different parts of the store. - assertLog(['x: foo, y: bar']); - - await act(() => { - 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". - assertLog(['x: bar, y: bar']); - }); - - // @gate enableUseMutableSource - 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.log('Render: ' + value); - React.useEffect(() => { - Scheduler.log('Commit: ' + value); - }, [value]); - - return value; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog(['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. - await waitForPaint(['Render: bar']); - // Then mutate B. - mutateB('baz'); - }); - assertLog([ - // 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'); - }); - - // @gate enableUseMutableSource - 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(() => { - 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. - await waitForPaint([]); - - // 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(() => { - ReactNoop.discreteUpdates(() => { - mutateA('a1'); - }); - }); - - expect(root).toMatchRenderedOutput('first: a1, second: a1'); - }); - - expect(root.getChildrenAsJSX()).toEqual('first: a1, second: a1'); - }); - - // @gate enableUseMutableSource - 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.log(value); - return value; - } - - function Text({text}) { - Scheduler.log(text); - return text; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render( - <> - - , - ); - }); - assertLog(['a0']); - expect(root).toMatchRenderedOutput('a0'); - - await act(async () => { - React.startTransition(() => { - root.render( - <> - - - - , - ); - }); - - await waitFor(['a0', 'b0']); - // Mutate in an event. This schedules a subscription update on a, which - // already mounted, but not b, which hasn't subscribed yet. - if (gate(flags => flags.enableUnifiedSyncLane)) { - React.startTransition(() => { - mutateA('a1'); - mutateB('b1'); - }); - } else { - 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 - await waitForPaint(['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. - await waitForPaint(['a0']); - - expect(root).toMatchRenderedOutput('a0b0c'); - // We should be done. - await waitForAll([]); - expect(root).toMatchRenderedOutput('a0b0c'); - }); - }, - ); - - // @gate enableUseMutableSource - 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.log(value); - return value; - } - - const root = ReactNoop.createRoot(); - root.render( - <> - - , - ); - await expect(async () => await waitForAll(['a'])).toErrorDev( - 'Mutable source should not return a function as the snapshot value.', - ); - expect(root).toMatchRenderedOutput('a'); - }); - - // @gate enableUseMutableSource - 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.log('Parent: ' + parentValue); - - return ( - - ); - } - - function Child({parentConfig, childConfig, parentValue}) { - const [getSnapshot, subscribe] = childConfig; - const childValue = useMutableSource( - mutableSource, - getSnapshot, - subscribe, - ); - - Scheduler.log('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.log('Commit: ' + result); - }, [result]); - - return result; - } - - const root = ReactNoop.createRoot(); - await act(() => { - root.render(); - }); - assertLog(['Parent: 1', 'Child: 2', 'Commit: 1, 2']); - - await act(async () => { - // Switch the parent and the child to read using the same config - React.startTransition(() => { - root.render(); - }); - // Start rendering the parent, but yield before rendering the child - await waitFor(['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.forceConcurrentByDefaultForTesting)) { - await waitFor([ - // 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. - // - await waitFor([ - 'Parent: 3', - // Demonstrates that we can yield here - ]); - await waitFor([ - // Now finish the rest of the update - 'Child: 3', - 'Commit: 3, 3', - ]); - } else { - // In default sync mode, all of the updates flush sync. - await waitFor([ - // The partial render completes - 'Child: 2', - 'Commit: 2, 2', - 'Parent: 3', - 'Child: 3', - ]); - - await waitForAll([ - // Now finish the rest of the update - 'Commit: 3, 3', - ]); - } - }); - }); - - // @gate enableUseMutableSource - 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.log(`a:${snapshot}`); - React.useEffect(() => { - committedA = snapshot; - }, [snapshot]); - return
    {`a:${snapshot}`}
    ; - } - function ComponentB() { - const snapshot = useMutableSource( - mutableSource, - defaultGetSnapshot, - defaultSubscribe, - ); - Scheduler.log(`b:${snapshot}`); - React.useEffect(() => { - committedB = snapshot; - }, [snapshot]); - return
    {`b:${snapshot}`}
    ; - } - - // Mount ComponentA with data version 1 - await act(() => { - ReactNoop.render( - - - , - () => Scheduler.log('Sync effect'), - ); - }); - assertLog(['a:one', 'Sync effect']); - expect(source.listenerCount).toBe(1); - - // Mount ComponentB with version 1 (but don't commit it) - await act(async () => { - ReactNoop.render( - - - - , - () => Scheduler.log('Sync effect'), - ); - await waitFor(['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 - await waitForAll(['a:two', 'b:two']); - expect(source.listenerCount).toBe(2); - }); - }); - - if (__DEV__) { - describe('dev warnings', () => { - // @gate enableUseMutableSource - it('should warn if the subscribe function does not return an unsubscribe function', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - const brokenSubscribe = () => {}; - - await expect(async () => { - await act(() => { - ReactNoop.render( - , - ); - }); - }).toErrorDev( - 'Mutable source subscribe function must return an unsubscribe function.', - ); - }); - - // @gate enableUseMutableSource - it('should error if multiple renderers of the same type use a mutable source at the same time', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - await act(async () => { - // Start a render that uses the mutable source. - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - - await waitFor(['a:one']); - - const PrevScheduler = Scheduler; - - // Get a new copy of ReactNoop. - loadModules(); - - spyOnDev(console, 'error').mockImplementation(() => {}); - - // Use the mutablesource again but with a different renderer. - ReactNoop.render( - , - ); - await waitFor(['c:one']); - - expect(console.error.mock.calls[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'); - }); - }); - - // @gate enableUseMutableSource - it('should error if multiple renderers of the same type use a mutable source at the same time with mutation between', async () => { - const source = createSource('one'); - const mutableSource = createMutableSource( - source, - param => param.version, - ); - - await act(async () => { - // Start a render that uses the mutable source. - React.startTransition(() => { - ReactNoop.render( - <> - - - , - ); - }); - - await waitFor(['a:one']); - - const PrevScheduler = Scheduler; - - // Get a new copy of ReactNoop. - loadModules(); - - spyOnDev(console, 'error').mockImplementation(() => {}); - - // Mutate before the new render reads from the source. - source.value = 'two'; - - // Use the mutablesource again but with a different renderer. - ReactNoop.render( - , - ); - await waitFor(['c:two']); - - expect(console.error.mock.calls[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 adf98fb4ede9..000000000000 --- a/packages/react-reconciler/src/__tests__/useMutableSourceHydration-test.js +++ /dev/null @@ -1,379 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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 ReactDOMClient; -let ReactDOMServer; -let Scheduler; -let act; -let createMutableSource; -let useMutableSource; -let waitFor; -let assertLog; - -describe('useMutableSourceHydration', () => { - beforeEach(() => { - jest.resetModules(); - - React = require('react'); - ReactDOMClient = require('react-dom/client'); - ReactDOMServer = require('react-dom/server'); - Scheduler = require('scheduler'); - - act = require('internal-test-utils').act; - - // Stable entrypoints export with "unstable_" prefix. - createMutableSource = - React.createMutableSource || React.unstable_createMutableSource; - useMutableSource = - React.useMutableSource || React.unstable_useMutableSource; - - const InternalTestUtils = require('internal-test-utils'); - waitFor = InternalTestUtils.waitFor; - assertLog = InternalTestUtils.assertLog; - }); - - 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.log(`${label}:${snapshot}`); - return
    {`${label}:${snapshot}`}
    ; - } - - // @gate enableUseMutableSource - it('should render and hydrate', async () => { - 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; - assertLog(['only:one']); - expect(source.listenerCount).toBe(0); - - await act(() => { - ReactDOMClient.hydrateRoot(container, , { - mutableSources: [mutableSource], - }); - }); - assertLog(['only:one']); - expect(source.listenerCount).toBe(1); - }); - - // @gate enableUseMutableSource - // @gate enableClientRenderFallbackOnTextMismatch - it('should detect a tear before hydrating a component', async () => { - 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; - assertLog(['only:one']); - expect(source.listenerCount).toBe(0); - - await expect(async () => { - await act(() => { - ReactDOMClient.hydrateRoot(container, , { - mutableSources: [mutableSource], - onRecoverableError(error) { - Scheduler.log('Log error: ' + error.message); - }, - }); - - source.value = 'two'; - }); - }).toErrorDev( - [ - 'Warning: Text content did not match. Server: "only:one" Client: "only:two"', - 'Warning: An error occurred during hydration. The server HTML was replaced with client content in
    .', - ], - {withoutStack: 1}, - ); - assertLog([ - 'only:two', - 'only:two', - 'Log error: Text content does not match server-rendered HTML.', - 'Log error: There was an error while hydrating. Because the error happened outside of a Suspense boundary, the entire root will switch to client rendering.', - ]); - expect(source.listenerCount).toBe(1); - }); - - // @gate enableUseMutableSource - it('should detect a tear between hydrating components', async () => { - 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; - assertLog(['a:one', 'b:one']); - expect(source.listenerCount).toBe(0); - - await expect(async () => { - await act(async () => { - React.startTransition(() => { - ReactDOMClient.hydrateRoot(container, , { - mutableSources: [mutableSource], - onRecoverableError(error) { - Scheduler.log('Log error: ' + error.message); - }, - }); - }); - - await waitFor(['a:one']); - source.value = 'two'; - }); - }).toErrorDev( - 'Warning: An error occurred during hydration. ' + - 'The server HTML was replaced with client content in
    .', - {withoutStack: true}, - ); - assertLog([ - 'a:two', - 'b:two', - // TODO: Before onRecoverableError, this error was never surfaced to the - // user. The request to file an bug report no longer makes sense. - // However, the experimental useMutableSource API is slated for - // removal, anyway. - 'Log error: Cannot read from mutable source during the current ' + - 'render without tearing. This may be a bug in React. Please file ' + - 'an issue.', - 'Log error: There was an error while hydrating. Because the error ' + - 'happened outside of a Suspense boundary, the entire root will ' + - 'switch to client rendering.', - ]); - expect(source.listenerCount).toBe(2); - }); - - // @gate enableUseMutableSource - it('should detect a tear between hydrating components reading from different parts of a source', async () => { - 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; - assertLog(['0:a:one', '1:b:one']); - - await expect(async () => { - await act(async () => { - const fragment = ( - <> - - - - ); - React.startTransition(() => { - ReactDOMClient.hydrateRoot(container, fragment, { - mutableSources: [mutableSource], - onRecoverableError(error) { - Scheduler.log('Log error: ' + error.message); - }, - }); - }); - await waitFor(['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}, - ); - assertLog([ - '0:a:one', - '1:b:two', - // TODO: Before onRecoverableError, this error was never surfaced to the - // user. The request to file an bug report no longer makes sense. - // However, the experimental useMutableSource API is slated for - // removal, anyway. - 'Log error: Cannot read from mutable source during the current ' + - 'render without tearing. This may be a bug in React. Please file ' + - 'an issue.', - 'Log error: There was an error while hydrating. Because the error ' + - 'happened outside of a Suspense boundary, the entire root will ' + - 'switch to client rendering.', - ]); - }); -}); diff --git a/packages/react-server/src/ReactFizzHooks.js b/packages/react-server/src/ReactFizzHooks.js index 78f68c917f27..f832aeb578fc 100644 --- a/packages/react-server/src/ReactFizzHooks.js +++ b/packages/react-server/src/ReactFizzHooks.js @@ -10,9 +10,6 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, ReactContext, StartTransitionOptions, Thenable, @@ -505,18 +502,6 @@ export function useEffectEvent) => Return>( return throwOnUseEffectEventCall; } -// 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, @@ -648,7 +633,6 @@ export const HooksDispatcher: Dispatcher = { useTransition, useId, // Subscriptions are not setup in a server environment. - useMutableSource, useSyncExternalStore, }; diff --git a/packages/react-server/src/ReactFlightHooks.js b/packages/react-server/src/ReactFlightHooks.js index 190292515efb..3042c6008cab 100644 --- a/packages/react-server/src/ReactFlightHooks.js +++ b/packages/react-server/src/ReactFlightHooks.js @@ -87,7 +87,6 @@ export const HooksDispatcher: Dispatcher = { useImperativeHandle: (unsupportedHook: any), useEffect: (unsupportedHook: any), useId, - useMutableSource: (unsupportedHook: any), useSyncExternalStore: (unsupportedHook: any), useCacheRefresh(): (?() => T, ?T) => void { return unsupportedRefresh; diff --git a/packages/react/index.classic.fb.js b/packages/react/index.classic.fb.js index 3afbd455e993..acc415ff563e 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, createServerContext, use, @@ -57,8 +55,6 @@ export { useLayoutEffect, useInsertionEffect, useMemo, - useMutableSource, - useMutableSource as unstable_useMutableSource, experimental_useOptimistic, useReducer, useRef, diff --git a/packages/react/index.js b/packages/react/index.js index d6eb4a5e14e1..fd24be44fc13 100644 --- a/packages/react/index.js +++ b/packages/react/index.js @@ -45,7 +45,6 @@ export { createContext, createElement, createFactory, - createMutableSource, createRef, createServerContext, use, @@ -76,7 +75,6 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, experimental_useOptimistic, useSyncExternalStore, useReducer, diff --git a/packages/react/index.modern.fb.js b/packages/react/index.modern.fb.js index aadda2063864..991bb68ea912 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, createServerContext, use, @@ -55,8 +53,6 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, - useMutableSource as unstable_useMutableSource, experimental_useOptimistic, useReducer, useRef, diff --git a/packages/react/src/React.js b/packages/react/src/React.js index 0e20df516b26..579ef6279c3d 100644 --- a/packages/react/src/React.js +++ b/packages/react/src/React.js @@ -48,7 +48,6 @@ import { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, useSyncExternalStore, useReducer, useRef, @@ -67,7 +66,6 @@ import { cloneElementWithValidation, } from './ReactElementValidator'; import {createServerContext} from './ReactServerContext'; -import {createMutableSource} from './ReactMutableSource'; import ReactSharedInternals from './ReactSharedInternals'; import {startTransition} from './ReactStartTransition'; import {act} from './ReactAct'; @@ -93,7 +91,6 @@ const Children = { export { Children, - createMutableSource, createRef, Component, PureComponent, @@ -112,7 +109,6 @@ export { useInsertionEffect, useLayoutEffect, useMemo, - useMutableSource, useOptimistic as experimental_useOptimistic, useSyncExternalStore, useReducer, diff --git a/packages/react/src/ReactHooks.js b/packages/react/src/ReactHooks.js index 62d41bc0bb6a..112baab1eaa5 100644 --- a/packages/react/src/ReactHooks.js +++ b/packages/react/src/ReactHooks.js @@ -9,9 +9,6 @@ import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes'; import type { - MutableSource, - MutableSourceGetSnapshotFn, - MutableSourceSubscribeFn, ReactContext, StartTransitionOptions, Usable, @@ -194,15 +191,6 @@ export function useId(): string { return dispatcher.useId(); } -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 164fbf55303a..000000000000 --- a/packages/react/src/ReactMutableSource.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Copyright (c) Meta Platforms, Inc. and 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/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 1236429619ae..86aaaa940326 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -32,9 +32,6 @@ export const enableComponentStackLocations = true; // TODO: Finish rolling out in www export const enableClientRenderFallbackOnTextMismatch = true; -// Recoil still uses useMutableSource in www, need to delete -export const enableUseMutableSource = false; - // Not sure if www still uses this. We don't have a replacement but whatever we // replace it with will likely be different than what's already there, so we // probably should just delete it as long as nothing in www relies on it. diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 0de363d98307..625d687e8829 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -7,8 +7,6 @@ * @flow */ -import type {Fiber} from 'react-reconciler/src/ReactInternalTypes'; - export type ReactNode = | React$Element | ReactPortal @@ -117,56 +115,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 { diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 0edbf1eea707..85f9ef17c329 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -74,8 +74,6 @@ export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableServerContext = true; -export const enableUseMutableSource = true; - export const enableTransitionTracing = false; export const enableFloat = true; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 1ad75cefa495..1ec92ff86e11 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -60,7 +60,6 @@ export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableServerContext = true; -export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.js index 67d35fbdd1e8..b1e9fc806358 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.js @@ -60,7 +60,6 @@ export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableServerContext = true; -export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js index 0954b32e9af0..049854054cfe 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native.js @@ -58,7 +58,6 @@ export const allowConcurrentByDefault = true; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableServerContext = true; -export const enableUseMutableSource = false; export const enableTransitionTracing = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js index 00808854a4d2..a702ec223d23 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.www.js @@ -61,9 +61,6 @@ export const enableCustomElementPropertySupport = false; export const consoleManagedByDevToolsDuringStrictMode = false; export const enableServerContext = true; -// Some www surfaces are still using this. Remove once they have been migrated. -export const enableUseMutableSource = true; - export const enableTransitionTracing = false; export const enableFloat = true; diff --git a/packages/shared/forks/ReactFeatureFlags.www.js b/packages/shared/forks/ReactFeatureFlags.www.js index 36b5236df831..48ca992b9b30 100644 --- a/packages/shared/forks/ReactFeatureFlags.www.js +++ b/packages/shared/forks/ReactFeatureFlags.www.js @@ -100,9 +100,6 @@ export const allowConcurrentByDefault = true; export const consoleManagedByDevToolsDuringStrictMode = true; export const enableServerContext = true; -// Some www surfaces are still using this. Remove once they have been migrated. -export const enableUseMutableSource = true; - export const useModernStrictMode = false; export const enableFizzExternalRuntime = true;