Skip to content

Commit 82c8fa9

Browse files
authored
Add back useMutableSource temporarily (#22396)
Recoil uses useMutableSource behind a flag. I thought this was fine because Recoil isn't used in any concurrent roots, so the behavior would be the same, but it turns out that it is used by concurrent roots in a few places. I'm not expecting it to be hard to migrate to useSyncExternalStore, but to de-risk the change I'm going to roll it out gradually with a flag. In the meantime, I've added back the useMutableSource API.
1 parent 8fcfdff commit 82c8fa9

37 files changed

+3732
-16
lines changed

packages/react-debug-tools/src/ReactDebugHooks.js

+25-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@
77
* @flow
88
*/
99

10-
import type {ReactContext, ReactProviderType} from 'shared/ReactTypes';
10+
import type {
11+
MutableSource,
12+
MutableSourceGetSnapshotFn,
13+
MutableSourceSubscribeFn,
14+
ReactContext,
15+
ReactProviderType,
16+
} from 'shared/ReactTypes';
1117
import type {
1218
Fiber,
1319
Dispatcher as DispatcherType,
@@ -255,6 +261,23 @@ function useMemo<T>(
255261
return value;
256262
}
257263

264+
function useMutableSource<Source, Snapshot>(
265+
source: MutableSource<Source>,
266+
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
267+
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
268+
): Snapshot {
269+
// useMutableSource() composes multiple hooks internally.
270+
// Advance the current hook index the same number of times
271+
// so that subsequent hooks have the right memoized state.
272+
nextHook(); // MutableSource
273+
nextHook(); // State
274+
nextHook(); // Effect
275+
nextHook(); // Effect
276+
const value = getSnapshot(source._source);
277+
hookLog.push({primitive: 'MutableSource', stackError: new Error(), value});
278+
return value;
279+
}
280+
258281
function useSyncExternalStore<T>(
259282
subscribe: (() => void) => () => void,
260283
getSnapshot: () => T,
@@ -335,6 +358,7 @@ const Dispatcher: DispatcherType = {
335358
useRef,
336359
useState,
337360
useTransition,
361+
useMutableSource,
338362
useSyncExternalStore,
339363
useDeferredValue,
340364
useOpaqueIdentifier,

packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js

+37
Original file line numberDiff line numberDiff line change
@@ -1019,6 +1019,43 @@ describe('ReactHooksInspectionIntegration', () => {
10191019
]);
10201020
});
10211021

1022+
it('should support composite useMutableSource hook', () => {
1023+
const createMutableSource =
1024+
React.createMutableSource || React.unstable_createMutableSource;
1025+
const useMutableSource =
1026+
React.useMutableSource || React.unstable_useMutableSource;
1027+
1028+
const mutableSource = createMutableSource({}, () => 1);
1029+
function Foo(props) {
1030+
useMutableSource(
1031+
mutableSource,
1032+
() => 'snapshot',
1033+
() => {},
1034+
);
1035+
React.useMemo(() => 'memo', []);
1036+
return <div />;
1037+
}
1038+
const renderer = ReactTestRenderer.create(<Foo />);
1039+
const childFiber = renderer.root.findByType(Foo)._currentFiber();
1040+
const tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
1041+
expect(tree).toEqual([
1042+
{
1043+
id: 0,
1044+
isStateEditable: false,
1045+
name: 'MutableSource',
1046+
value: 'snapshot',
1047+
subHooks: [],
1048+
},
1049+
{
1050+
id: 1,
1051+
isStateEditable: false,
1052+
name: 'Memo',
1053+
value: 'memo',
1054+
subHooks: [],
1055+
},
1056+
]);
1057+
});
1058+
10221059
// @gate experimental || www
10231060
it('should support composite useSyncExternalStore hook', () => {
10241061
const useSyncExternalStore = React.unstable_useSyncExternalStore;

packages/react-dom/src/client/ReactDOMRoot.js

+26-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import type {Container} from './ReactDOMHostConfig';
11-
import type {ReactNodeList} from 'shared/ReactTypes';
11+
import type {MutableSource, ReactNodeList} from 'shared/ReactTypes';
1212
import type {FiberRoot} from 'react-reconciler/src/ReactInternalTypes';
1313

1414
export type RootType = {
@@ -24,6 +24,7 @@ export type CreateRootOptions = {
2424
hydrationOptions?: {
2525
onHydrated?: (suspenseNode: Comment) => void,
2626
onDeleted?: (suspenseNode: Comment) => void,
27+
mutableSources?: Array<MutableSource<any>>,
2728
...
2829
},
2930
// END OF TODO
@@ -34,6 +35,7 @@ export type CreateRootOptions = {
3435

3536
export type HydrateRootOptions = {
3637
// Hydration options
38+
hydratedSources?: Array<MutableSource<any>>,
3739
onHydrated?: (suspenseNode: Comment) => void,
3840
onDeleted?: (suspenseNode: Comment) => void,
3941
// Options for all roots
@@ -59,6 +61,7 @@ import {
5961
createContainer,
6062
updateContainer,
6163
findHostInstanceWithNoPortals,
64+
registerMutableSourceForHydration,
6265
} from 'react-reconciler/src/ReactFiberReconciler';
6366
import invariant from 'shared/invariant';
6467
import {ConcurrentRoot} from 'react-reconciler/src/ReactRootTags';
@@ -126,6 +129,11 @@ export function createRoot(
126129
const hydrate = options != null && options.hydrate === true;
127130
const hydrationCallbacks =
128131
(options != null && options.hydrationOptions) || null;
132+
const mutableSources =
133+
(options != null &&
134+
options.hydrationOptions != null &&
135+
options.hydrationOptions.mutableSources) ||
136+
null;
129137
// END TODO
130138

131139
const isStrictMode = options != null && options.unstable_strictMode === true;
@@ -151,6 +159,15 @@ export function createRoot(
151159
container.nodeType === COMMENT_NODE ? container.parentNode : container;
152160
listenToAllSupportedEvents(rootContainerElement);
153161

162+
// TODO: Delete this path
163+
if (mutableSources) {
164+
for (let i = 0; i < mutableSources.length; i++) {
165+
const mutableSource = mutableSources[i];
166+
registerMutableSourceForHydration(root, mutableSource);
167+
}
168+
}
169+
// END TODO
170+
154171
return new ReactDOMRoot(root);
155172
}
156173

@@ -168,6 +185,7 @@ export function hydrateRoot(
168185
// For now we reuse the whole bag of options since they contain
169186
// the hydration callbacks.
170187
const hydrationCallbacks = options != null ? options : null;
188+
const mutableSources = (options != null && options.hydratedSources) || null;
171189
const isStrictMode = options != null && options.unstable_strictMode === true;
172190

173191
let concurrentUpdatesByDefaultOverride = null;
@@ -190,6 +208,13 @@ export function hydrateRoot(
190208
// This can't be a comment node since hydration doesn't work on comment nodes anyway.
191209
listenToAllSupportedEvents(container);
192210

211+
if (mutableSources) {
212+
for (let i = 0; i < mutableSources.length; i++) {
213+
const mutableSource = mutableSources[i];
214+
registerMutableSourceForHydration(root, mutableSource);
215+
}
216+
}
217+
193218
// Render the initial children
194219
updateContainer(initialChildren, root, null, null);
195220

packages/react-dom/src/server/ReactPartialRendererHooks.js

+20-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99

1010
import type {Dispatcher as DispatcherType} from 'react-reconciler/src/ReactInternalTypes';
1111

12-
import type {ReactContext} from 'shared/ReactTypes';
12+
import type {
13+
MutableSource,
14+
MutableSourceGetSnapshotFn,
15+
MutableSourceSubscribeFn,
16+
ReactContext,
17+
} from 'shared/ReactTypes';
1318
import type PartialRenderer from './ReactPartialRenderer';
1419

1520
import {validateContextBounds} from './ReactPartialRendererContext';
@@ -461,6 +466,18 @@ export function useCallback<T>(
461466
return useMemo(() => callback, deps);
462467
}
463468

469+
// TODO Decide on how to implement this hook for server rendering.
470+
// If a mutation occurs during render, consider triggering a Suspense boundary
471+
// and falling back to client rendering.
472+
function useMutableSource<Source, Snapshot>(
473+
source: MutableSource<Source>,
474+
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
475+
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
476+
): Snapshot {
477+
resolveCurrentlyRenderingComponent();
478+
return getSnapshot(source._source);
479+
}
480+
464481
function useSyncExternalStore<T>(
465482
subscribe: (() => void) => () => void,
466483
getSnapshot: () => T,
@@ -527,6 +544,8 @@ export const Dispatcher: DispatcherType = {
527544
useDeferredValue,
528545
useTransition,
529546
useOpaqueIdentifier,
547+
// Subscriptions are not setup in a server environment.
548+
useMutableSource,
530549
useSyncExternalStore,
531550
};
532551

packages/react-reconciler/src/ReactFiberBeginWork.new.js

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
1212
import type {Fiber, FiberRoot} from './ReactInternalTypes';
1313
import type {TypeOfMode} from './ReactTypeOfMode';
1414
import type {Lanes, Lane} from './ReactFiberLane.new';
15+
import type {MutableSource} from 'shared/ReactTypes';
1516
import type {
1617
SuspenseState,
1718
SuspenseListRenderState,
@@ -144,6 +145,7 @@ import {
144145
isSuspenseInstancePending,
145146
isSuspenseInstanceFallback,
146147
registerSuspenseInstanceRetry,
148+
supportsHydration,
147149
isPrimaryRenderer,
148150
supportsPersistence,
149151
getOffscreenContainerProps,
@@ -218,6 +220,7 @@ import {
218220
RetryAfterError,
219221
NoContext,
220222
} from './ReactFiberWorkLoop.new';
223+
import {setWorkInProgressVersion} from './ReactMutableSource.new';
221224
import {
222225
requestCacheFromPool,
223226
pushCacheProvider,
@@ -1297,6 +1300,21 @@ function updateHostRoot(current, workInProgress, renderLanes) {
12971300
// We always try to hydrate. If this isn't a hydration pass there won't
12981301
// be any children to hydrate which is effectively the same thing as
12991302
// not hydrating.
1303+
1304+
if (supportsHydration) {
1305+
const mutableSourceEagerHydrationData =
1306+
root.mutableSourceEagerHydrationData;
1307+
if (mutableSourceEagerHydrationData != null) {
1308+
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1309+
const mutableSource = ((mutableSourceEagerHydrationData[
1310+
i
1311+
]: any): MutableSource<any>);
1312+
const version = mutableSourceEagerHydrationData[i + 1];
1313+
setWorkInProgressVersion(mutableSource, version);
1314+
}
1315+
}
1316+
}
1317+
13001318
const child = mountChildFibers(
13011319
workInProgress,
13021320
null,

packages/react-reconciler/src/ReactFiberBeginWork.old.js

+18
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type {LazyComponent as LazyComponentType} from 'react/src/ReactLazy';
1212
import type {Fiber, FiberRoot} from './ReactInternalTypes';
1313
import type {TypeOfMode} from './ReactTypeOfMode';
1414
import type {Lanes, Lane} from './ReactFiberLane.old';
15+
import type {MutableSource} from 'shared/ReactTypes';
1516
import type {
1617
SuspenseState,
1718
SuspenseListRenderState,
@@ -144,6 +145,7 @@ import {
144145
isSuspenseInstancePending,
145146
isSuspenseInstanceFallback,
146147
registerSuspenseInstanceRetry,
148+
supportsHydration,
147149
isPrimaryRenderer,
148150
supportsPersistence,
149151
getOffscreenContainerProps,
@@ -218,6 +220,7 @@ import {
218220
RetryAfterError,
219221
NoContext,
220222
} from './ReactFiberWorkLoop.old';
223+
import {setWorkInProgressVersion} from './ReactMutableSource.old';
221224
import {
222225
requestCacheFromPool,
223226
pushCacheProvider,
@@ -1297,6 +1300,21 @@ function updateHostRoot(current, workInProgress, renderLanes) {
12971300
// We always try to hydrate. If this isn't a hydration pass there won't
12981301
// be any children to hydrate which is effectively the same thing as
12991302
// not hydrating.
1303+
1304+
if (supportsHydration) {
1305+
const mutableSourceEagerHydrationData =
1306+
root.mutableSourceEagerHydrationData;
1307+
if (mutableSourceEagerHydrationData != null) {
1308+
for (let i = 0; i < mutableSourceEagerHydrationData.length; i += 2) {
1309+
const mutableSource = ((mutableSourceEagerHydrationData[
1310+
i
1311+
]: any): MutableSource<any>);
1312+
const version = mutableSourceEagerHydrationData[i + 1];
1313+
setWorkInProgressVersion(mutableSource, version);
1314+
}
1315+
}
1316+
}
1317+
13001318
const child = mountChildFibers(
13011319
workInProgress,
13021320
null,

packages/react-reconciler/src/ReactFiberCompleteWork.new.js

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import type {SuspenseContext} from './ReactFiberSuspenseContext.new';
3030
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3131
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.new';
3232

33+
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.new';
34+
3335
import {now} from './Scheduler';
3436

3537
import {
@@ -852,6 +854,7 @@ function completeWork(
852854
}
853855
popHostContainer(workInProgress);
854856
popTopLevelLegacyContextObject(workInProgress);
857+
resetMutableSourceWorkInProgressVersions();
855858
if (fiberRoot.pendingContext) {
856859
fiberRoot.context = fiberRoot.pendingContext;
857860
fiberRoot.pendingContext = null;

packages/react-reconciler/src/ReactFiberCompleteWork.old.js

+3
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import type {SuspenseContext} from './ReactFiberSuspenseContext.old';
3030
import type {OffscreenState} from './ReactFiberOffscreenComponent';
3131
import type {Cache, SpawnedCachePool} from './ReactFiberCacheComponent.old';
3232

33+
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource.old';
34+
3335
import {now} from './Scheduler';
3436

3537
import {
@@ -852,6 +854,7 @@ function completeWork(
852854
}
853855
popHostContainer(workInProgress);
854856
popTopLevelLegacyContextObject(workInProgress);
857+
resetMutableSourceWorkInProgressVersions();
855858
if (fiberRoot.pendingContext) {
856859
fiberRoot.context = fiberRoot.pendingContext;
857860
fiberRoot.pendingContext = null;

0 commit comments

Comments
 (0)