@samitouri / QOS-React-2 / commits / 263cfa6ecb

[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,