Skip to content

Commit 322cdcd

Browse files
author
Brian Vaughn
authored
useMutableSource hook (facebook#18000)
useMutableSource hook useMutableSource() enables React components to safely and efficiently read from a mutable external source in Concurrent Mode. The API will detect mutations that occur during a render to avoid tearing and it will automatically schedule updates when the source is mutated. RFC: reactjs/rfcs#147
1 parent 30a998d commit 322cdcd

24 files changed

+2062
-27
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
*/
99

1010
import type {
11+
MutableSource,
12+
MutableSourceGetSnapshotFn,
13+
MutableSourceSubscribeFn,
1114
ReactContext,
1215
ReactProviderType,
1316
ReactEventResponder,
@@ -72,6 +75,16 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
7275
Dispatcher.useDebugValue(null);
7376
Dispatcher.useCallback(() => {});
7477
Dispatcher.useMemo(() => null);
78+
Dispatcher.useMutableSource(
79+
{
80+
_source: {},
81+
_getVersion: () => 1,
82+
_workInProgressVersionPrimary: null,
83+
_workInProgressVersionSecondary: null,
84+
},
85+
() => null,
86+
() => () => {},
87+
);
7588
} finally {
7689
readHookLog = hookLog;
7790
hookLog = [];
@@ -229,6 +242,23 @@ function useMemo<T>(
229242
return value;
230243
}
231244

245+
function useMutableSource<Source, Snapshot>(
246+
source: MutableSource<Source>,
247+
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
248+
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
249+
): Snapshot {
250+
// useMutableSource() composes multiple hooks internally.
251+
// Advance the current hook index the same number of times
252+
// so that subsequent hooks have the right memoized state.
253+
nextHook(); // MutableSource
254+
nextHook(); // State
255+
nextHook(); // Effect
256+
nextHook(); // Effect
257+
const value = getSnapshot(source._source);
258+
hookLog.push({primitive: 'MutableSource', stackError: new Error(), value});
259+
return value;
260+
}
261+
232262
function useResponder(
233263
responder: ReactEventResponder<any, any>,
234264
listenerProps: Object,
@@ -299,6 +329,7 @@ const Dispatcher: DispatcherType = {
299329
useState,
300330
useResponder,
301331
useTransition,
332+
useMutableSource,
302333
useDeferredValue,
303334
useEvent,
304335
};

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,4 +785,38 @@ describe('ReactHooksInspectionIntegration', () => {
785785
},
786786
]);
787787
});
788+
789+
if (__EXPERIMENTAL__) {
790+
it('should support composite useMutableSource hook', () => {
791+
const mutableSource = React.createMutableSource({}, () => 1);
792+
function Foo(props) {
793+
React.useMutableSource(
794+
mutableSource,
795+
() => 'snapshot',
796+
() => {},
797+
);
798+
React.useMemo(() => 'memo', []);
799+
return <div />;
800+
}
801+
let renderer = ReactTestRenderer.create(<Foo />);
802+
let childFiber = renderer.root.findByType(Foo)._currentFiber();
803+
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
804+
expect(tree).toEqual([
805+
{
806+
id: 0,
807+
isStateEditable: false,
808+
name: 'MutableSource',
809+
value: 'snapshot',
810+
subHooks: [],
811+
},
812+
{
813+
id: 1,
814+
isStateEditable: false,
815+
name: 'Memo',
816+
value: 'memo',
817+
subHooks: [],
818+
},
819+
]);
820+
});
821+
}
788822
});

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ import type {
1313
} from 'react-reconciler/src/ReactFiberHooks';
1414
import type {ThreadID} from './ReactThreadIDAllocator';
1515
import type {
16+
MutableSource,
17+
MutableSourceGetSnapshotFn,
18+
MutableSourceSubscribeFn,
1619
ReactContext,
1720
ReactEventResponderListener,
1821
} from 'shared/ReactTypes';
@@ -461,6 +464,18 @@ function useResponder(responder, props): ReactEventResponderListener<any, any> {
461464
};
462465
}
463466

467+
// TODO Decide on how to implement this hook for server rendering.
468+
// If a mutation occurs during render, consider triggering a Suspense boundary
469+
// and falling back to client rendering.
470+
function useMutableSource<Source, Snapshot>(
471+
source: MutableSource<Source>,
472+
getSnapshot: MutableSourceGetSnapshotFn<Source, Snapshot>,
473+
subscribe: MutableSourceSubscribeFn<Source, Snapshot>,
474+
): Snapshot {
475+
resolveCurrentlyRenderingComponent();
476+
return getSnapshot(source._source);
477+
}
478+
464479
function useDeferredValue<T>(value: T, config: TimeoutConfig | null | void): T {
465480
resolveCurrentlyRenderingComponent();
466481
return value;
@@ -510,4 +525,6 @@ export const Dispatcher: DispatcherType = {
510525
useDeferredValue,
511526
useTransition,
512527
useEvent,
528+
// Subscriptions are not setup in a server environment.
529+
useMutableSource,
513530
};

packages/react-reconciler/src/ReactFiberBeginWork.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ import {
180180
markSpawnedWork,
181181
requestCurrentTimeForUpdate,
182182
retryDehydratedSuspenseBoundary,
183-
scheduleWork,
183+
scheduleUpdateOnFiber,
184184
renderDidSuspendDelayIfPossible,
185185
markUnprocessedUpdateTime,
186186
} from './ReactFiberWorkLoop';
@@ -2121,7 +2121,7 @@ function updateDehydratedSuspenseComponent(
21212121
// at even higher pri.
21222122
let attemptHydrationAtExpirationTime = renderExpirationTime + 1;
21232123
suspenseState.retryTime = attemptHydrationAtExpirationTime;
2124-
scheduleWork(current, attemptHydrationAtExpirationTime);
2124+
scheduleUpdateOnFiber(current, attemptHydrationAtExpirationTime);
21252125
// TODO: Early abort this render.
21262126
} else {
21272127
// We have already tried to ping at a higher priority than we're rendering with

packages/react-reconciler/src/ReactFiberClassComponent.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ import {readContext} from './ReactFiberNewContext';
5353
import {
5454
requestCurrentTimeForUpdate,
5555
computeExpirationForFiber,
56-
scheduleWork,
56+
scheduleUpdateOnFiber,
5757
} from './ReactFiberWorkLoop';
5858
import {requestCurrentSuspenseConfig} from './ReactFiberSuspenseConfig';
5959

@@ -200,7 +200,7 @@ const classComponentUpdater = {
200200
}
201201

202202
enqueueUpdate(fiber, update);
203-
scheduleWork(fiber, expirationTime);
203+
scheduleUpdateOnFiber(fiber, expirationTime);
204204
},
205205
enqueueReplaceState(inst, payload, callback) {
206206
const fiber = getInstance(inst);
@@ -224,7 +224,7 @@ const classComponentUpdater = {
224224
}
225225

226226
enqueueUpdate(fiber, update);
227-
scheduleWork(fiber, expirationTime);
227+
scheduleUpdateOnFiber(fiber, expirationTime);
228228
},
229229
enqueueForceUpdate(inst, callback) {
230230
const fiber = getInstance(inst);
@@ -247,7 +247,7 @@ const classComponentUpdater = {
247247
}
248248

249249
enqueueUpdate(fiber, update);
250-
scheduleWork(fiber, expirationTime);
250+
scheduleUpdateOnFiber(fiber, expirationTime);
251251
},
252252
};
253253

packages/react-reconciler/src/ReactFiberCompleteWork.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import type {
2626
SuspenseListRenderState,
2727
} from './ReactFiberSuspenseComponent';
2828
import type {SuspenseContext} from './ReactFiberSuspenseContext';
29+
import {resetWorkInProgressVersions as resetMutableSourceWorkInProgressVersions} from './ReactMutableSource';
2930

3031
import {now} from './SchedulerWithReactIntegration';
3132

@@ -662,6 +663,7 @@ function completeWork(
662663
case HostRoot: {
663664
popHostContainer(workInProgress);
664665
popTopLevelLegacyContextObject(workInProgress);
666+
resetMutableSourceWorkInProgressVersions();
665667
const fiberRoot = (workInProgress.stateNode: FiberRoot);
666668
if (fiberRoot.pendingContext) {
667669
fiberRoot.context = fiberRoot.pendingContext;

0 commit comments

Comments
 (0)