[Experimental] Add useInsertionEffect (#21913)
Ricky committed
Sep 14, 2021 at 10:27 UTC
263cfa6ecb9879ecb629d4e04a8c26422b4c4ff9
20 files changed
+862
-6
packages/react-debug-tools/src/ReactDebugHooks.js
+14
@@ -78,6 +78,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
78
Dispatcher.useCacheRefresh();
79
}
80
Dispatcher.useLayoutEffect(() => {});
81
+ Dispatcher.useInsertionEffect(() => {});
82
Dispatcher.useEffect(() => {});
83
Dispatcher.useImperativeHandle(undefined, () => null);
84
Dispatcher.useDebugValue(null);
@@ -191,6 +192,18 @@ function useLayoutEffect(
192
});
193
}
194
195
+function useInsertionEffect(
196
+ create: () => mixed,
197
+ inputs: Array<mixed> | void | null,
198
+): void {
199
+ nextHook();
200
+ hookLog.push({
201
+ primitive: 'InsertionEffect',
202
+ stackError: new Error(),
203
+ value: create,
204
+ });
205
+}
206
+
207
function useEffect(
208
create: () => (() => void) | void,
209
inputs: Array<mixed> | void | null,
@@ -338,6 +351,7 @@ const Dispatcher: DispatcherType = {
351
useImperativeHandle,
352
useDebugValue,
353
useLayoutEffect,
354
+ useInsertionEffect,
355
useMemo,
356
useReducer,
357
useRef,
packages/react-debug-tools/src/__tests__/ReactHooksInspectionIntegration-test.js
+177
@@ -268,6 +268,183 @@ describe('ReactHooksInspectionIntegration', () => {
268
]);
269
});
270
271
+ // @gate experimental || www
272
+ it('should inspect the current state of all stateful hooks, including useInsertionEffect', () => {
273
+ const useInsertionEffect = React.unstable_useInsertionEffect;
274
+ const outsideRef = React.createRef();
275
+ function effect() {}
276
+ function Foo(props) {
277
+ const [state1, setState] = React.useState('a');
278
+ const [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
279
+ const ref = React.useRef('c');
280
+
281
+ useInsertionEffect(effect);
282
+ React.useLayoutEffect(effect);
283
+ React.useEffect(effect);
284
+
285
+ React.useImperativeHandle(
286
+ outsideRef,
287
+ () => {
288
+ // Return a function so that jest treats them as non-equal.
289
+ return function Instance() {};
290
+ },
291
+ [],
292
+ );
293
+
294
+ React.useMemo(() => state1 + state2, [state1]);
295
+
296
+ function update() {
297
+ act(() => {
298
+ setState('A');
299
+ });
300
+ act(() => {
301
+ dispatch({value: 'B'});
302
+ });
303
+ ref.current = 'C';
304
+ }
305
+ const memoizedUpdate = React.useCallback(update, []);
306
+ return (
307
+ <div onClick={memoizedUpdate}>
308
+ {state1} {state2}
309
+ </div>
310
+ );
311
+ }
312
+ let renderer;
313
+ act(() => {
314
+ renderer = ReactTestRenderer.create(<Foo prop="prop" />);
315
+ });
316
+
317
+ let childFiber = renderer.root.findByType(Foo)._currentFiber();
318
+
319
+ const {onClick: updateStates} = renderer.root.findByType('div').props;
320
+
321
+ let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
322
+ expect(tree).toEqual([
323
+ {
324
+ isStateEditable: true,
325
+ id: 0,
326
+ name: 'State',
327
+ value: 'a',
328
+ subHooks: [],
329
+ },
330
+ {
331
+ isStateEditable: true,
332
+ id: 1,
333
+ name: 'Reducer',
334
+ value: 'b',
335
+ subHooks: [],
336
+ },
337
+ {isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
338
+ {
339
+ isStateEditable: false,
340
+ id: 3,
341
+ name: 'InsertionEffect',
342
+ value: effect,
343
+ subHooks: [],
344
+ },
345
+ {
346
+ isStateEditable: false,
347
+ id: 4,
348
+ name: 'LayoutEffect',
349
+ value: effect,
350
+ subHooks: [],
351
+ },
352
+ {
353
+ isStateEditable: false,
354
+ id: 5,
355
+ name: 'Effect',
356
+ value: effect,
357
+ subHooks: [],
358
+ },
359
+ {
360
+ isStateEditable: false,
361
+ id: 6,
362
+ name: 'ImperativeHandle',
363
+ value: outsideRef.current,
364
+ subHooks: [],
365
+ },
366
+ {
367
+ isStateEditable: false,
368
+ id: 7,
369
+ name: 'Memo',
370
+ value: 'ab',
371
+ subHooks: [],
372
+ },
373
+ {
374
+ isStateEditable: false,
375
+ id: 8,
376
+ name: 'Callback',
377
+ value: updateStates,
378
+ subHooks: [],
379
+ },
380
+ ]);
381
+
382
+ updateStates();
383
+
384
+ childFiber = renderer.root.findByType(Foo)._currentFiber();
385
+ tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
386
+
387
+ expect(tree).toEqual([
388
+ {
389
+ isStateEditable: true,
390
+ id: 0,
391
+ name: 'State',
392
+ value: 'A',
393
+ subHooks: [],
394
+ },
395
+ {
396
+ isStateEditable: true,
397
+ id: 1,
398
+ name: 'Reducer',
399
+ value: 'B',
400
+ subHooks: [],
401
+ },
402
+ {isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
403
+ {
404
+ isStateEditable: false,
405
+ id: 3,
406
+ name: 'InsertionEffect',
407
+ value: effect,
408
+ subHooks: [],
409
+ },
410
+ {
411
+ isStateEditable: false,
412
+ id: 4,
413
+ name: 'LayoutEffect',
414
+ value: effect,
415
+ subHooks: [],
416
+ },
417
+ {
418
+ isStateEditable: false,
419
+ id: 5,
420
+ name: 'Effect',
421
+ value: effect,
422
+ subHooks: [],
423
+ },
424
+ {
425
+ isStateEditable: false,
426
+ id: 6,
427
+ name: 'ImperativeHandle',
428
+ value: outsideRef.current,
429
+ subHooks: [],
430
+ },
431
+ {
432
+ isStateEditable: false,
433
+ id: 7,
434
+ name: 'Memo',
435
+ value: 'Ab',
436
+ subHooks: [],
437
+ },
438
+ {
439
+ isStateEditable: false,
440
+ id: 8,
441
+ name: 'Callback',
442
+ value: updateStates,
443
+ subHooks: [],
444
+ },
445
+ ]);
446
+ });
447
+
448
it('should inspect the value of the current provider in useContext', () => {
449
const MyContext = React.createContext('default');
450
function Foo(props) {
packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js
+18
@@ -27,6 +27,7 @@ let useCallback;
27
let useMemo;
28
let useRef;
29
let useImperativeHandle;
30
+let useInsertionEffect;
31
let useLayoutEffect;
32
let useDebugValue;
33
let useOpaqueIdentifier;
@@ -54,6 +55,7 @@ function initModules() {
55
useRef = React.useRef;
56
useDebugValue = React.useDebugValue;
57
useImperativeHandle = React.useImperativeHandle;
58
+ useInsertionEffect = React.unstable_useInsertionEffect;
59
useLayoutEffect = React.useLayoutEffect;
60
useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
61
forwardRef = React.forwardRef;
@@ -638,6 +640,22 @@ describe('ReactDOMServerHooks', () => {
640
expect(domNode.textContent).toEqual('Count: 0');
641
});
642
});
643
+ describe('useInsertionEffect', () => {
644
+ // @gate experimental || www
645
+ it('should warn when invoked during render', async () => {
646
+ function Counter() {
647
+ useInsertionEffect(() => {
648
+ throw new Error('should not be invoked');
649
+ });
650
+
651
+ return <Text text="Count: 0" />;
652
+ }
653
+ const domNode = await serverRender(<Counter />, 1);
654
+ expect(clearYields()).toEqual(['Count: 0']);
655
+ expect(domNode.tagName).toEqual('SPAN');
656
+ expect(domNode.textContent).toEqual('Count: 0');
657
+ });
658
+ });
659
660
describe('useLayoutEffect', () => {
661
it('should warn when invoked during render', async () => {
packages/react-dom/src/server/ReactPartialRendererHooks.js
+17
@@ -385,6 +385,22 @@ function useRef<T>(initialValue: T): {|current: T|} {
385
}
386
}
387
388
+function useInsertionEffect(
389
+ create: () => mixed,
390
+ inputs: Array<mixed> | void | null,
391
+) {
392
+ if (__DEV__) {
393
+ currentHookNameInDev = 'useInsertionEffect';
394
+ console.error(
395
+ 'useInsertionEffect does nothing on the server, because its effect cannot ' +
396
+ "be encoded into the server renderer's output format. This will lead " +
397
+ 'to a mismatch between the initial, non-hydrated UI and the intended ' +
398
+ 'UI. To avoid this, useInsertionEffect should only be used in ' +
399
+ 'components that render exclusively on the client.',
400
+ );
401
+ }
402
+}
403
+
404
export function useLayoutEffect(
405
create: () => (() => void) | void,
406
inputs: Array<mixed> | void | null,
@@ -508,6 +524,7 @@ export const Dispatcher: DispatcherType = {
524
useReducer,
525
useRef,
526
useState,
527
+ useInsertionEffect,
528
useLayoutEffect,
529
useCallback,
530
// useImperativeHandle is not run in the server environment
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+20
-1
@@ -136,6 +136,7 @@ import {
136
NoFlags as NoHookEffect,
137
HasEffect as HookHasEffect,
138
Layout as HookLayout,
139
+ Insertion as HookInsertion,
140
Passive as HookPassive,
141
} from './ReactHookEffectTags';
142
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
@@ -525,6 +526,8 @@ function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
526
let hookName;
527
if ((effect.tag & HookLayout) !== NoFlags) {
528
hookName = 'useLayoutEffect';
529
+ } else if ((effect.tag & HookInsertion) !== NoFlags) {
530
+ hookName = 'useInsertionEffect';
531
} else {
532
hookName = 'useEffect';
533
}
@@ -1153,7 +1156,10 @@ function commitUnmount(
1156
do {
1157
const {destroy, tag} = effect;
1158
if (destroy !== undefined) {
1156
- if ((tag & HookLayout) !== NoHookEffect) {
1159
+ if (
1160
+ (tag & HookInsertion) !== NoHookEffect ||
1161
+ (tag & HookLayout) !== NoHookEffect
1162
+ ) {
1163
if (
1164
enableProfilerTimer &&
1165
enableProfilerCommitHooks &&
@@ -1738,6 +1744,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1744
case ForwardRef:
1745
case MemoComponent:
1746
case SimpleMemoComponent: {
1747
+ commitHookEffectListUnmount(
1748
+ HookInsertion | HookHasEffect,
1749
+ finishedWork,
1750
+ finishedWork.return,
1751
+ );
1752
+ commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1753
+
1754
// Layout effects are destroyed during the mutation phase so that all
1755
// destroy functions for all fibers are called before any create functions.
1756
// This prevents sibling component effects from interfering with each other,
@@ -1810,6 +1823,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1823
case ForwardRef:
1824
case MemoComponent:
1825
case SimpleMemoComponent: {
1826
+ commitHookEffectListUnmount(
1827
+ HookInsertion | HookHasEffect,
1828
+ finishedWork,
1829
+ finishedWork.return,
1830
+ );
1831
+ commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1832
// Layout effects are destroyed during the mutation phase so that all
1833
// destroy functions for all fibers are called before any create functions.
1834
// This prevents sibling component effects from interfering with each other,
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+20
-1
@@ -136,6 +136,7 @@ import {
136
NoFlags as NoHookEffect,
137
HasEffect as HookHasEffect,
138
Layout as HookLayout,
139
+ Insertion as HookInsertion,
140
Passive as HookPassive,
141
} from './ReactHookEffectTags';
142
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
@@ -525,6 +526,8 @@ function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
526
let hookName;
527
if ((effect.tag & HookLayout) !== NoFlags) {
528
hookName = 'useLayoutEffect';
529
+ } else if ((effect.tag & HookInsertion) !== NoFlags) {
530
+ hookName = 'useInsertionEffect';
531
} else {
532
hookName = 'useEffect';
533
}
@@ -1153,7 +1156,10 @@ function commitUnmount(
1156
do {
1157
const {destroy, tag} = effect;
1158
if (destroy !== undefined) {
1156
- if ((tag & HookLayout) !== NoHookEffect) {
1159
+ if (
1160
+ (tag & HookInsertion) !== NoHookEffect ||
1161
+ (tag & HookLayout) !== NoHookEffect
1162
+ ) {
1163
if (
1164
enableProfilerTimer &&
1165
enableProfilerCommitHooks &&
@@ -1738,6 +1744,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1744
case ForwardRef:
1745
case MemoComponent:
1746
case SimpleMemoComponent: {
1747
+ commitHookEffectListUnmount(
1748
+ HookInsertion | HookHasEffect,
1749
+ finishedWork,
1750
+ finishedWork.return,
1751
+ );
1752
+ commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1753
+
1754
// Layout effects are destroyed during the mutation phase so that all
1755
// destroy functions for all fibers are called before any create functions.
1756
// This prevents sibling component effects from interfering with each other,
@@ -1810,6 +1823,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
1823
case ForwardRef:
1824
case MemoComponent:
1825
case SimpleMemoComponent: {
1826
+ commitHookEffectListUnmount(
1827
+ HookInsertion | HookHasEffect,
1828
+ finishedWork,
1829
+ finishedWork.return,
1830
+ );
1831
+ commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1832
// Layout effects are destroyed during the mutation phase so that all
1833
// destroy functions for all fibers are called before any create functions.
1834
// This prevents sibling component effects from interfering with each other,
packages/react-reconciler/src/ReactFiberHooks.new.js
+79
@@ -75,6 +75,7 @@ import {
75
HasEffect as HookHasEffect,
76
Layout as HookLayout,
77
Passive as HookPassive,
78
+ Insertion as HookInsertion,
79
} from './ReactHookEffectTags';
80
import {
81
getWorkInProgressRoot,
@@ -1708,6 +1709,20 @@ function updateEffect(
1709
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1710
}
1711
1712
+function mountInsertionEffect(
1713
+ create: () => (() => void) | void,
1714
+ deps: Array<mixed> | void | null,
1715
+): void {
1716
+ return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
1717
+}
1718
+
1719
+function updateInsertionEffect(
1720
+ create: () => (() => void) | void,
1721
+ deps: Array<mixed> | void | null,
1722
+): void {
1723
+ return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
1724
+}
1725
+
1726
function mountLayoutEffect(
1727
create: () => (() => void) | void,
1728
deps: Array<mixed> | void | null,
@@ -2310,6 +2325,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2325
useContext: throwInvalidHookError,
2326
useEffect: throwInvalidHookError,
2327
useImperativeHandle: throwInvalidHookError,
2328
+ useInsertionEffect: throwInvalidHookError,
2329
useLayoutEffect: throwInvalidHookError,
2330
useMemo: throwInvalidHookError,
2331
useReducer: throwInvalidHookError,
@@ -2337,6 +2353,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2353
useEffect: mountEffect,
2354
useImperativeHandle: mountImperativeHandle,
2355
useLayoutEffect: mountLayoutEffect,
2356
+ useInsertionEffect: mountInsertionEffect,
2357
useMemo: mountMemo,
2358
useReducer: mountReducer,
2359
useRef: mountRef,
@@ -2362,6 +2379,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2379
useContext: readContext,
2380
useEffect: updateEffect,
2381
useImperativeHandle: updateImperativeHandle,
2382
+ useInsertionEffect: updateInsertionEffect,
2383
useLayoutEffect: updateLayoutEffect,
2384
useMemo: updateMemo,
2385
useReducer: updateReducer,
@@ -2388,6 +2406,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2406
useContext: readContext,
2407
useEffect: updateEffect,
2408
useImperativeHandle: updateImperativeHandle,
2409
+ useInsertionEffect: updateInsertionEffect,
2410
useLayoutEffect: updateLayoutEffect,
2411
useMemo: updateMemo,
2412
useReducer: rerenderReducer,
@@ -2468,6 +2487,15 @@ if (__DEV__) {
2487
checkDepsAreArrayDev(deps);
2488
return mountImperativeHandle(ref, create, deps);
2489
},
2490
+ useInsertionEffect(
2491
+ create: () => (() => void) | void,
2492
+ deps: Array<mixed> | void | null,
2493
+ ): void {
2494
+ currentHookNameInDev = 'useInsertionEffect';
2495
+ mountHookTypesDev();
2496
+ checkDepsAreArrayDev(deps);
2497
+ return mountInsertionEffect(create, deps);
2498
+ },
2499
useLayoutEffect(
2500
create: () => (() => void) | void,
2501
deps: Array<mixed> | void | null,
@@ -2602,6 +2630,14 @@ if (__DEV__) {
2630
updateHookTypesDev();
2631
return mountImperativeHandle(ref, create, deps);
2632
},
2633
+ useInsertionEffect(
2634
+ create: () => (() => void) | void,
2635
+ deps: Array<mixed> | void | null,
2636
+ ): void {
2637
+ currentHookNameInDev = 'useInsertionEffect';
2638
+ updateHookTypesDev();
2639
+ return mountInsertionEffect(create, deps);
2640
+ },
2641
useLayoutEffect(
2642
create: () => (() => void) | void,
2643
deps: Array<mixed> | void | null,
@@ -2734,6 +2770,14 @@ if (__DEV__) {
2770
updateHookTypesDev();
2771
return updateImperativeHandle(ref, create, deps);
2772
},
2773
+ useInsertionEffect(
2774
+ create: () => (() => void) | void,
2775
+ deps: Array<mixed> | void | null,
2776
+ ): void {
2777
+ currentHookNameInDev = 'useInsertionEffect';
2778
+ updateHookTypesDev();
2779
+ return updateInsertionEffect(create, deps);
2780
+ },
2781
useLayoutEffect(
2782
create: () => (() => void) | void,
2783
deps: Array<mixed> | void | null,
@@ -2867,6 +2911,14 @@ if (__DEV__) {
2911
updateHookTypesDev();
2912
return updateImperativeHandle(ref, create, deps);
2913
},
2914
+ useInsertionEffect(
2915
+ create: () => (() => void) | void,
2916
+ deps: Array<mixed> | void | null,
2917
+ ): void {
2918
+ currentHookNameInDev = 'useInsertionEffect';
2919
+ updateHookTypesDev();
2920
+ return updateInsertionEffect(create, deps);
2921
+ },
2922
useLayoutEffect(
2923
create: () => (() => void) | void,
2924
deps: Array<mixed> | void | null,
@@ -3004,6 +3056,15 @@ if (__DEV__) {
3056
mountHookTypesDev();
3057
return mountImperativeHandle(ref, create, deps);
3058
},
3059
+ useInsertionEffect(
3060
+ create: () => (() => void) | void,
3061
+ deps: Array<mixed> | void | null,
3062
+ ): void {
3063
+ currentHookNameInDev = 'useInsertionEffect';
3064
+ warnInvalidHookAccess();
3065
+ mountHookTypesDev();
3066
+ return mountInsertionEffect(create, deps);
3067
+ },
3068
useLayoutEffect(
3069
create: () => (() => void) | void,
3070
deps: Array<mixed> | void | null,
@@ -3152,6 +3213,15 @@ if (__DEV__) {
3213
updateHookTypesDev();
3214
return updateImperativeHandle(ref, create, deps);
3215
},
3216
+ useInsertionEffect(
3217
+ create: () => (() => void) | void,
3218
+ deps: Array<mixed> | void | null,
3219
+ ): void {
3220
+ currentHookNameInDev = 'useInsertionEffect';
3221
+ warnInvalidHookAccess();
3222
+ updateHookTypesDev();
3223
+ return updateInsertionEffect(create, deps);
3224
+ },
3225
useLayoutEffect(
3226
create: () => (() => void) | void,
3227
deps: Array<mixed> | void | null,
@@ -3301,6 +3371,15 @@ if (__DEV__) {
3371
updateHookTypesDev();
3372
return updateImperativeHandle(ref, create, deps);
3373
},
3374
+ useInsertionEffect(
3375
+ create: () => (() => void) | void,
3376
+ deps: Array<mixed> | void | null,
3377
+ ): void {
3378
+ currentHookNameInDev = 'useInsertionEffect';
3379
+ warnInvalidHookAccess();
3380
+ updateHookTypesDev();
3381
+ return updateInsertionEffect(create, deps);
3382
+ },
3383
useLayoutEffect(
3384
create: () => (() => void) | void,
3385
deps: Array<mixed> | void | null,
packages/react-reconciler/src/ReactFiberHooks.old.js
+79
@@ -75,6 +75,7 @@ import {
75
HasEffect as HookHasEffect,
76
Layout as HookLayout,
77
Passive as HookPassive,
78
+ Insertion as HookInsertion,
79
} from './ReactHookEffectTags';
80
import {
81
getWorkInProgressRoot,
@@ -1708,6 +1709,20 @@ function updateEffect(
1709
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
1710
}
1711
1712
+function mountInsertionEffect(
1713
+ create: () => (() => void) | void,
1714
+ deps: Array<mixed> | void | null,
1715
+): void {
1716
+ return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
1717
+}
1718
+
1719
+function updateInsertionEffect(
1720
+ create: () => (() => void) | void,
1721
+ deps: Array<mixed> | void | null,
1722
+): void {
1723
+ return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
1724
+}
1725
+
1726
function mountLayoutEffect(
1727
create: () => (() => void) | void,
1728
deps: Array<mixed> | void | null,
@@ -2310,6 +2325,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
2325
useContext: throwInvalidHookError,
2326
useEffect: throwInvalidHookError,
2327
useImperativeHandle: throwInvalidHookError,
2328
+ useInsertionEffect: throwInvalidHookError,
2329
useLayoutEffect: throwInvalidHookError,
2330
useMemo: throwInvalidHookError,
2331
useReducer: throwInvalidHookError,
@@ -2337,6 +2353,7 @@ const HooksDispatcherOnMount: Dispatcher = {
2353
useEffect: mountEffect,
2354
useImperativeHandle: mountImperativeHandle,
2355
useLayoutEffect: mountLayoutEffect,
2356
+ useInsertionEffect: mountInsertionEffect,
2357
useMemo: mountMemo,
2358
useReducer: mountReducer,
2359
useRef: mountRef,
@@ -2362,6 +2379,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
2379
useContext: readContext,
2380
useEffect: updateEffect,
2381
useImperativeHandle: updateImperativeHandle,
2382
+ useInsertionEffect: updateInsertionEffect,
2383
useLayoutEffect: updateLayoutEffect,
2384
useMemo: updateMemo,
2385
useReducer: updateReducer,
@@ -2388,6 +2406,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
2406
useContext: readContext,
2407
useEffect: updateEffect,
2408
useImperativeHandle: updateImperativeHandle,
2409
+ useInsertionEffect: updateInsertionEffect,
2410
useLayoutEffect: updateLayoutEffect,
2411
useMemo: updateMemo,
2412
useReducer: rerenderReducer,
@@ -2468,6 +2487,15 @@ if (__DEV__) {
2487
checkDepsAreArrayDev(deps);
2488
return mountImperativeHandle(ref, create, deps);
2489
},
2490
+ useInsertionEffect(
2491
+ create: () => (() => void) | void,
2492
+ deps: Array<mixed> | void | null,
2493
+ ): void {
2494
+ currentHookNameInDev = 'useInsertionEffect';
2495
+ mountHookTypesDev();
2496
+ checkDepsAreArrayDev(deps);
2497
+ return mountInsertionEffect(create, deps);
2498
+ },
2499
useLayoutEffect(
2500
create: () => (() => void) | void,
2501
deps: Array<mixed> | void | null,
@@ -2602,6 +2630,14 @@ if (__DEV__) {
2630
updateHookTypesDev();
2631
return mountImperativeHandle(ref, create, deps);
2632
},
2633
+ useInsertionEffect(
2634
+ create: () => (() => void) | void,
2635
+ deps: Array<mixed> | void | null,
2636
+ ): void {
2637
+ currentHookNameInDev = 'useInsertionEffect';
2638
+ updateHookTypesDev();
2639
+ return mountInsertionEffect(create, deps);
2640
+ },
2641
useLayoutEffect(
2642
create: () => (() => void) | void,
2643
deps: Array<mixed> | void | null,
@@ -2734,6 +2770,14 @@ if (__DEV__) {
2770
updateHookTypesDev();
2771
return updateImperativeHandle(ref, create, deps);
2772
},
2773
+ useInsertionEffect(
2774
+ create: () => (() => void) | void,
2775
+ deps: Array<mixed> | void | null,
2776
+ ): void {
2777
+ currentHookNameInDev = 'useInsertionEffect';
2778
+ updateHookTypesDev();
2779
+ return updateInsertionEffect(create, deps);
2780
+ },
2781
useLayoutEffect(
2782
create: () => (() => void) | void,
2783
deps: Array<mixed> | void | null,
@@ -2867,6 +2911,14 @@ if (__DEV__) {
2911
updateHookTypesDev();
2912
return updateImperativeHandle(ref, create, deps);
2913
},
2914
+ useInsertionEffect(
2915
+ create: () => (() => void) | void,
2916
+ deps: Array<mixed> | void | null,
2917
+ ): void {
2918
+ currentHookNameInDev = 'useInsertionEffect';
2919
+ updateHookTypesDev();
2920
+ return updateInsertionEffect(create, deps);
2921
+ },
2922
useLayoutEffect(
2923
create: () => (() => void) | void,
2924
deps: Array<mixed> | void | null,
@@ -3004,6 +3056,15 @@ if (__DEV__) {
3056
mountHookTypesDev();
3057
return mountImperativeHandle(ref, create, deps);
3058
},
3059
+ useInsertionEffect(
3060
+ create: () => (() => void) | void,
3061
+ deps: Array<mixed> | void | null,
3062
+ ): void {
3063
+ currentHookNameInDev = 'useInsertionEffect';
3064
+ warnInvalidHookAccess();
3065
+ mountHookTypesDev();
3066
+ return mountInsertionEffect(create, deps);
3067
+ },
3068
useLayoutEffect(
3069
create: () => (() => void) | void,
3070
deps: Array<mixed> | void | null,
@@ -3152,6 +3213,15 @@ if (__DEV__) {
3213
updateHookTypesDev();
3214
return updateImperativeHandle(ref, create, deps);
3215
},
3216
+ useInsertionEffect(
3217
+ create: () => (() => void) | void,
3218
+ deps: Array<mixed> | void | null,
3219
+ ): void {
3220
+ currentHookNameInDev = 'useInsertionEffect';
3221
+ warnInvalidHookAccess();
3222
+ updateHookTypesDev();
3223
+ return updateInsertionEffect(create, deps);
3224
+ },
3225
useLayoutEffect(
3226
create: () => (() => void) | void,
3227
deps: Array<mixed> | void | null,
@@ -3301,6 +3371,15 @@ if (__DEV__) {
3371
updateHookTypesDev();
3372
return updateImperativeHandle(ref, create, deps);
3373
},
3374
+ useInsertionEffect(
3375
+ create: () => (() => void) | void,
3376
+ deps: Array<mixed> | void | null,
3377
+ ): void {
3378
+ currentHookNameInDev = 'useInsertionEffect';
3379
+ warnInvalidHookAccess();
3380
+ updateHookTypesDev();
3381
+ return updateInsertionEffect(create, deps);
3382
+ },
3383
useLayoutEffect(
3384
create: () => (() => void) | void,
3385
deps: Array<mixed> | void | null,
packages/react-reconciler/src/ReactHookEffectTags.js
+5
-4
@@ -9,11 +9,12 @@
9
10
export type HookFlags = number;
11
12
-export const NoFlags = /* */ 0b000;
12
+export const NoFlags = /* */ 0b0000;
13
14
// Represents whether effect should fire.
15
-export const HasEffect = /* */ 0b001;
15
+export const HasEffect = /* */ 0b0001;
16
17
// Represents the phase in which the effect (not the clean-up) fires.
18
-export const Layout = /* */ 0b010;
19
-export const Passive = /* */ 0b100;
18
+export const Insertion = /* */ 0b0010;
19
+export const Layout = /* */ 0b0100;
20
+export const Passive = /* */ 0b1000;
packages/react-reconciler/src/ReactInternalTypes.js
+5
@@ -33,6 +33,7 @@ export type HookType =
33
| 'useContext'
34
| 'useRef'
35
| 'useEffect'
36
+ | 'useInsertionEffect'
37
| 'useLayoutEffect'
38
| 'useCallback'
39
| 'useMemo'
@@ -286,6 +287,10 @@ export type Dispatcher = {|
287
create: () => (() => void) | void,
288
deps: Array<mixed> | void | null,
289
): void,
290
+ useInsertionEffect(
291
+ create: () => (() => void) | void,
292
+ deps: Array<mixed> | void | null,
293
+ ): void,
294
useLayoutEffect(
295
create: () => (() => void) | void,
296
deps: Array<mixed> | void | null,
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+411
@@ -22,6 +22,7 @@ let Suspense;
22
let useState;
23
let useReducer;
24
let useEffect;
25
+let useInsertionEffect;
26
let useLayoutEffect;
27
let useCallback;
28
let useMemo;
@@ -46,6 +47,7 @@ describe('ReactHooksWithNoopRenderer', () => {
47
useState = React.useState;
48
useReducer = React.useReducer;
49
useEffect = React.useEffect;
50
+ useInsertionEffect = React.unstable_useInsertionEffect;
51
useLayoutEffect = React.useLayoutEffect;
52
useCallback = React.useCallback;
53
useMemo = React.useMemo;
@@ -2682,6 +2684,415 @@ describe('ReactHooksWithNoopRenderer', () => {
2684
});
2685
});
2686
2687
+ describe('useInsertionEffect', () => {
2688
+ // @gate experimental || www
2689
+ it('fires insertion effects after snapshots on update', () => {
2690
+ function CounterA(props) {
2691
+ useInsertionEffect(() => {
2692
+ Scheduler.unstable_yieldValue(`Create insertion`);
2693
+ return () => {
2694
+ Scheduler.unstable_yieldValue(`Destroy insertion`);
2695
+ };
2696
+ });
2697
+ return null;
2698
+ }
2699
+
2700
+ class CounterB extends React.Component {
2701
+ getSnapshotBeforeUpdate(prevProps, prevState) {
2702
+ Scheduler.unstable_yieldValue(`Get Snapshot`);
2703
+ return null;
2704
+ }
2705
+
2706
+ componentDidUpdate() {}
2707
+
2708
+ render() {
2709
+ return null;
2710
+ }
2711
+ }
2712
+
2713
+ act(() => {
2714
+ ReactNoop.render(
2715
+ <>
2716
+ <CounterA />
2717
+ <CounterB />
2718
+ </>,
2719
+ );
2720
+
2721
+ expect(Scheduler).toFlushAndYield(['Create insertion']);
2722
+ });
2723
+
2724
+ // Update
2725
+ act(() => {
2726
+ ReactNoop.render(
2727
+ <>
2728
+ <CounterA />
2729
+ <CounterB />
2730
+ </>,
2731
+ );
2732
+
2733
+ expect(Scheduler).toFlushAndYield([
2734
+ 'Get Snapshot',
2735
+ 'Destroy insertion',
2736
+ 'Create insertion',
2737
+ ]);
2738
+ });
2739
+
2740
+ // Unmount everything
2741
+ act(() => {
2742
+ ReactNoop.render(null);
2743
+
2744
+ expect(Scheduler).toFlushAndYield(['Destroy insertion']);
2745
+ });
2746
+ });
2747
+
2748
+ // @gate experimental || www
2749
+ it('fires insertion effects before layout effects', () => {
2750
+ let committedText = '(empty)';
2751
+
2752
+ function Counter(props) {
2753
+ useInsertionEffect(() => {
2754
+ Scheduler.unstable_yieldValue(
2755
+ `Create insertion [current: ${committedText}]`,
2756
+ );
2757
+ committedText = props.count + '';
2758
+ return () => {
2759
+ Scheduler.unstable_yieldValue(
2760
+ `Destroy insertion [current: ${committedText}]`,
2761
+ );
2762
+ };
2763
+ });
2764
+ useLayoutEffect(() => {
2765
+ Scheduler.unstable_yieldValue(
2766
+ `Create layout [current: ${committedText}]`,
2767
+ );
2768
+ return () => {
2769
+ Scheduler.unstable_yieldValue(
2770
+ `Destroy layout [current: ${committedText}]`,
2771
+ );
2772
+ };
2773
+ });
2774
+ useEffect(() => {
2775
+ Scheduler.unstable_yieldValue(
2776
+ `Create passive [current: ${committedText}]`,
2777
+ );
2778
+ return () => {
2779
+ Scheduler.unstable_yieldValue(
2780
+ `Destroy passive [current: ${committedText}]`,
2781
+ );
2782
+ };
2783
+ });
2784
+ return null;
2785
+ }
2786
+ act(() => {
2787
+ ReactNoop.render(<Counter count={0} />);
2788
+
2789
+ expect(Scheduler).toFlushUntilNextPaint([
2790
+ 'Create insertion [current: (empty)]',
2791
+ 'Create layout [current: 0]',
2792
+ ]);
2793
+ expect(committedText).toEqual('0');
2794
+ });
2795
+
2796
+ expect(Scheduler).toHaveYielded(['Create passive [current: 0]']);
2797
+
2798
+ // Unmount everything
2799
+ act(() => {
2800
+ ReactNoop.render(null);
2801
+
2802
+ expect(Scheduler).toFlushUntilNextPaint([
2803
+ 'Destroy insertion [current: 0]',
2804
+ 'Destroy layout [current: 0]',
2805
+ ]);
2806
+ });
2807
+
2808
+ expect(Scheduler).toHaveYielded(['Destroy passive [current: 0]']);
2809
+ });
2810
+
2811
+ // @gate experimental || www
2812
+ it('force flushes passive effects before firing new insertion effects', () => {
2813
+ let committedText = '(empty)';
2814
+
2815
+ function Counter(props) {
2816
+ useInsertionEffect(() => {
2817
+ Scheduler.unstable_yieldValue(
2818
+ `Create insertion [current: ${committedText}]`,
2819
+ );
2820
+ committedText = props.count + '';
2821
+ return () => {
2822
+ Scheduler.unstable_yieldValue(
2823
+ `Destroy insertion [current: ${committedText}]`,
2824
+ );
2825
+ };
2826
+ });
2827
+ useLayoutEffect(() => {
2828
+ Scheduler.unstable_yieldValue(
2829
+ `Create layout [current: ${committedText}]`,
2830
+ );
2831
+ committedText = props.count + '';
2832
+ return () => {
2833
+ Scheduler.unstable_yieldValue(
2834
+ `Destroy layout [current: ${committedText}]`,
2835
+ );
2836
+ };
2837
+ });
2838
+ useEffect(() => {
2839
+ Scheduler.unstable_yieldValue(
2840
+ `Create passive [current: ${committedText}]`,
2841
+ );
2842
+ return () => {
2843
+ Scheduler.unstable_yieldValue(
2844
+ `Destroy passive [current: ${committedText}]`,
2845
+ );
2846
+ };
2847
+ });
2848
+ return null;
2849
+ }
2850
+
2851
+ act(() => {
2852
+ React.startTransition(() => {
2853
+ ReactNoop.render(<Counter count={0} />);
2854
+ });
2855
+ expect(Scheduler).toFlushUntilNextPaint([
2856
+ 'Create insertion [current: (empty)]',
2857
+ 'Create layout [current: 0]',
2858
+ ]);
2859
+ expect(committedText).toEqual('0');
2860
+
2861
+ React.startTransition(() => {
2862
+ ReactNoop.render(<Counter count={1} />);
2863
+ });
2864
+ expect(Scheduler).toFlushUntilNextPaint([
2865
+ 'Create passive [current: 0]',
2866
+ 'Destroy insertion [current: 0]',
2867
+ 'Create insertion [current: 0]',
2868
+ 'Destroy layout [current: 1]',
2869
+ 'Create layout [current: 1]',
2870
+ ]);
2871
+ expect(committedText).toEqual('1');
2872
+ });
2873
+ expect(Scheduler).toHaveYielded([
2874
+ 'Destroy passive [current: 1]',
2875
+ 'Create passive [current: 1]',
2876
+ ]);
2877
+ });
2878
+
2879
+ // @gate experimental || www
2880
+ it('fires all insertion effects (interleaved) before firing any layout effects', () => {
2881
+ let committedA = '(empty)';
2882
+ let committedB = '(empty)';
2883
+
2884
+ function CounterA(props) {
2885
+ useInsertionEffect(() => {
2886
+ Scheduler.unstable_yieldValue(
2887
+ `Create Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
2888
+ );
2889
+ committedA = props.count + '';
2890
+ return () => {
2891
+ Scheduler.unstable_yieldValue(
2892
+ `Destroy Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
2893
+ );
2894
+ };
2895
+ });
2896
+ useInsertionEffect(() => {
2897
+ Scheduler.unstable_yieldValue(
2898
+ `Create Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
2899
+ );
2900
+ committedA = props.count + '';
2901
+ return () => {
2902
+ Scheduler.unstable_yieldValue(
2903
+ `Destroy Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
2904
+ );
2905
+ };
2906
+ });
2907
+
2908
+ useLayoutEffect(() => {
2909
+ Scheduler.unstable_yieldValue(
2910
+ `Create Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
2911
+ );
2912
+ return () => {
2913
+ Scheduler.unstable_yieldValue(
2914
+ `Destroy Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
2915
+ );
2916
+ };
2917
+ });
2918
+
2919
+ useLayoutEffect(() => {
2920
+ Scheduler.unstable_yieldValue(
2921
+ `Create Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
2922
+ );
2923
+ return () => {
2924
+ Scheduler.unstable_yieldValue(
2925
+ `Destroy Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
2926
+ );
2927
+ };
2928
+ });
2929
+ return null;
2930
+ }
2931
+
2932
+ function CounterB(props) {
2933
+ useInsertionEffect(() => {
2934
+ Scheduler.unstable_yieldValue(
2935
+ `Create Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
2936
+ );
2937
+ committedB = props.count + '';
2938
+ return () => {
2939
+ Scheduler.unstable_yieldValue(
2940
+ `Destroy Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
2941
+ );
2942
+ };
2943
+ });
2944
+ useInsertionEffect(() => {
2945
+ Scheduler.unstable_yieldValue(
2946
+ `Create Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
2947
+ );
2948
+ committedB = props.count + '';
2949
+ return () => {
2950
+ Scheduler.unstable_yieldValue(
2951
+ `Destroy Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
2952
+ );
2953
+ };
2954
+ });
2955
+
2956
+ useLayoutEffect(() => {
2957
+ Scheduler.unstable_yieldValue(
2958
+ `Create Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
2959
+ );
2960
+ return () => {
2961
+ Scheduler.unstable_yieldValue(
2962
+ `Destroy Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
2963
+ );
2964
+ };
2965
+ });
2966
+
2967
+ useLayoutEffect(() => {
2968
+ Scheduler.unstable_yieldValue(
2969
+ `Create Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
2970
+ );
2971
+ return () => {
2972
+ Scheduler.unstable_yieldValue(
2973
+ `Destroy Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
2974
+ );
2975
+ };
2976
+ });
2977
+ return null;
2978
+ }
2979
+
2980
+ act(() => {
2981
+ ReactNoop.render(
2982
+ <React.Fragment>
2983
+ <CounterA count={0} />
2984
+ <CounterB count={0} />
2985
+ </React.Fragment>,
2986
+ );
2987
+ expect(Scheduler).toFlushAndYield([
2988
+ // All insertion effects fire before all layout effects
2989
+ 'Create Insertion 1 for Component A [A: (empty), B: (empty)]',
2990
+ 'Create Insertion 2 for Component A [A: 0, B: (empty)]',
2991
+ 'Create Insertion 1 for Component B [A: 0, B: (empty)]',
2992
+ 'Create Insertion 2 for Component B [A: 0, B: 0]',
2993
+ 'Create Layout 1 for Component A [A: 0, B: 0]',
2994
+ 'Create Layout 2 for Component A [A: 0, B: 0]',
2995
+ 'Create Layout 1 for Component B [A: 0, B: 0]',
2996
+ 'Create Layout 2 for Component B [A: 0, B: 0]',
2997
+ ]);
2998
+ expect([committedA, committedB]).toEqual(['0', '0']);
2999
+ });
3000
+
3001
+ act(() => {
3002
+ ReactNoop.render(
3003
+ <React.Fragment>
3004
+ <CounterA count={1} />
3005
+ <CounterB count={1} />
3006
+ </React.Fragment>,
3007
+ );
3008
+ expect(Scheduler).toFlushAndYield([
3009
+ 'Destroy Insertion 1 for Component A [A: 0, B: 0]',
3010
+ 'Destroy Insertion 2 for Component A [A: 0, B: 0]',
3011
+ 'Create Insertion 1 for Component A [A: 0, B: 0]',
3012
+ 'Create Insertion 2 for Component A [A: 1, B: 0]',
3013
+ 'Destroy Layout 1 for Component A [A: 1, B: 0]',
3014
+ 'Destroy Layout 2 for Component A [A: 1, B: 0]',
3015
+ 'Destroy Insertion 1 for Component B [A: 1, B: 0]',
3016
+ 'Destroy Insertion 2 for Component B [A: 1, B: 0]',
3017
+ 'Create Insertion 1 for Component B [A: 1, B: 0]',
3018
+ 'Create Insertion 2 for Component B [A: 1, B: 1]',
3019
+ 'Destroy Layout 1 for Component B [A: 1, B: 1]',
3020
+ 'Destroy Layout 2 for Component B [A: 1, B: 1]',
3021
+ 'Create Layout 1 for Component A [A: 1, B: 1]',
3022
+ 'Create Layout 2 for Component A [A: 1, B: 1]',
3023
+ 'Create Layout 1 for Component B [A: 1, B: 1]',
3024
+ 'Create Layout 2 for Component B [A: 1, B: 1]',
3025
+ ]);
3026
+ expect([committedA, committedB]).toEqual(['1', '1']);
3027
+
3028
+ // Unmount everything
3029
+ act(() => {
3030
+ ReactNoop.render(null);
3031
+
3032
+ expect(Scheduler).toFlushAndYield([
3033
+ 'Destroy Insertion 1 for Component A [A: 1, B: 1]',
3034
+ 'Destroy Insertion 2 for Component A [A: 1, B: 1]',
3035
+ 'Destroy Layout 1 for Component A [A: 1, B: 1]',
3036
+ 'Destroy Layout 2 for Component A [A: 1, B: 1]',
3037
+ 'Destroy Insertion 1 for Component B [A: 1, B: 1]',
3038
+ 'Destroy Insertion 2 for Component B [A: 1, B: 1]',
3039
+ 'Destroy Layout 1 for Component B [A: 1, B: 1]',
3040
+ 'Destroy Layout 2 for Component B [A: 1, B: 1]',
3041
+ ]);
3042
+ });
3043
+ });
3044
+ });
3045
+
3046
+ // @gate experimental || www
3047
+ it('assumes insertion effect destroy function is either a function or undefined', () => {
3048
+ function App(props) {
3049
+ useInsertionEffect(() => {
3050
+ return props.return;
3051
+ });
3052
+ return null;
3053
+ }
3054
+
3055
+ const root1 = ReactNoop.createRoot();
3056
+ expect(() =>
3057
+ act(() => {
3058
+ root1.render(<App return={17} />);
3059
+ }),
3060
+ ).toErrorDev([
3061
+ 'Warning: useInsertionEffect must not return anything besides a ' +
3062
+ 'function, which is used for clean-up. You returned: 17',
3063
+ ]);
3064
+
3065
+ const root2 = ReactNoop.createRoot();
3066
+ expect(() =>
3067
+ act(() => {
3068
+ root2.render(<App return={null} />);
3069
+ }),
3070
+ ).toErrorDev([
3071
+ 'Warning: useInsertionEffect must not return anything besides a ' +
3072
+ 'function, which is used for clean-up. You returned null. If your ' +
3073
+ 'effect does not require clean up, return undefined (or nothing).',
3074
+ ]);
3075
+
3076
+ const root3 = ReactNoop.createRoot();
3077
+ expect(() =>
3078
+ act(() => {
3079
+ root3.render(<App return={Promise.resolve()} />);
3080
+ }),
3081
+ ).toErrorDev([
3082
+ 'Warning: useInsertionEffect must not return anything besides a ' +
3083
+ 'function, which is used for clean-up.\n\n' +
3084
+ 'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
3085
+ ]);
3086
+
3087
+ // Error on unmount because React assumes the value is a function
3088
+ expect(() =>
3089
+ act(() => {
3090
+ root3.unmount();
3091
+ }),
3092
+ ).toThrow('is not a function');
3093
+ });
3094
+ });
3095
+
3096
describe('useLayoutEffect', () => {
3097
it('fires layout effects after the host has been mutated', () => {
3098
function getCommittedText() {
packages/react-server/src/ReactFizzHooks.js
+1
@@ -503,6 +503,7 @@ export const Dispatcher: DispatcherType = {
503
useReducer,
504
useRef,
505
useState,
506
+ useInsertionEffect: noop,
507
useLayoutEffect,
508
useCallback,
509
// useImperativeHandle is not run in the server environment
packages/react-server/src/ReactFlightServer.js
+1
@@ -822,6 +822,7 @@ const Dispatcher: DispatcherType = {
822
useReducer: (unsupportedHook: any),
823
useRef: (unsupportedHook: any),
824
useState: (unsupportedHook: any),
825
+ useInsertionEffect: (unsupportedHook: any),
826
useLayoutEffect: (unsupportedHook: any),
827
useImperativeHandle: (unsupportedHook: any),
828
useEffect: (unsupportedHook: any),
packages/react-suspense-test-utils/src/ReactSuspenseTestUtils.js
+1
@@ -35,6 +35,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
35
useReducer: unsupported,
36
useRef: unsupported,
37
useState: unsupported,
38
+ useInsertionEffect: unsupported,
39
useLayoutEffect: unsupported,
40
useCallback: unsupported,
41
useImperativeHandle: unsupported,
packages/react/index.classic.fb.js
+1
@@ -48,6 +48,7 @@ export {
48
useEffect,
49
useImperativeHandle,
50
useLayoutEffect,
51
+ unstable_useInsertionEffect,
52
useMemo,
53
useMutableSource,
54
useMutableSource as unstable_useMutableSource,
packages/react/index.experimental.js
+1
@@ -42,6 +42,7 @@ export {
42
useDeferredValue,
43
useEffect,
44
useImperativeHandle,
45
+ unstable_useInsertionEffect,
46
useLayoutEffect,
47
useMemo,
48
useMutableSource as unstable_useMutableSource,
packages/react/index.js
+1
@@ -67,6 +67,7 @@ export {
67
useDeferredValue,
68
useEffect,
69
useImperativeHandle,
70
+ unstable_useInsertionEffect,
71
useLayoutEffect,
72
useMemo,
73
useMutableSource,
packages/react/index.modern.fb.js
+1
@@ -46,6 +46,7 @@ export {
46
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
47
useEffect,
48
useImperativeHandle,
49
+ unstable_useInsertionEffect,
50
useLayoutEffect,
51
useMemo,
52
useMutableSource,
packages/react/src/React.js
+2
@@ -41,6 +41,7 @@ import {
41
useEffect,
42
useImperativeHandle,
43
useDebugValue,
44
+ useInsertionEffect,
45
useLayoutEffect,
46
useMemo,
47
useMutableSource,
@@ -91,6 +92,7 @@ export {
92
useEffect,
93
useImperativeHandle,
94
useDebugValue,
95
+ useInsertionEffect as unstable_useInsertionEffect,
96
useLayoutEffect,
97
useMemo,
98
useMutableSource,
packages/react/src/ReactHooks.js
+8
@@ -100,6 +100,14 @@ export function useEffect(
100
return dispatcher.useEffect(create, deps);
101
}
102
103
+export function useInsertionEffect(
104
+ create: () => (() => void) | void,
105
+ deps: Array<mixed> | void | null,
106
+): void {
107
+ const dispatcher = resolveDispatcher();
108
+ return dispatcher.useInsertionEffect(create, deps);
109
+}
110
+
111
export function useLayoutEffect(
112
create: () => (() => void) | void,
113
deps: Array<mixed> | void | null,