Skip to content

Commit 263cfa6

Browse files
authored
[Experimental] Add useInsertionEffect (#21913)
1 parent 806aaa2 commit 263cfa6

20 files changed

+862
-6
lines changed

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

+14
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
7878
Dispatcher.useCacheRefresh();
7979
}
8080
Dispatcher.useLayoutEffect(() => {});
81+
Dispatcher.useInsertionEffect(() => {});
8182
Dispatcher.useEffect(() => {});
8283
Dispatcher.useImperativeHandle(undefined, () => null);
8384
Dispatcher.useDebugValue(null);
@@ -191,6 +192,18 @@ function useLayoutEffect(
191192
});
192193
}
193194

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+
194207
function useEffect(
195208
create: () => (() => void) | void,
196209
inputs: Array<mixed> | void | null,
@@ -338,6 +351,7 @@ const Dispatcher: DispatcherType = {
338351
useImperativeHandle,
339352
useDebugValue,
340353
useLayoutEffect,
354+
useInsertionEffect,
341355
useMemo,
342356
useReducer,
343357
useRef,

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

+177
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,183 @@ describe('ReactHooksInspectionIntegration', () => {
268268
]);
269269
});
270270

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+
271448
it('should inspect the value of the current provider in useContext', () => {
272449
const MyContext = React.createContext('default');
273450
function Foo(props) {

packages/react-dom/src/__tests__/ReactDOMServerIntegrationHooks-test.js

+18
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let useCallback;
2727
let useMemo;
2828
let useRef;
2929
let useImperativeHandle;
30+
let useInsertionEffect;
3031
let useLayoutEffect;
3132
let useDebugValue;
3233
let useOpaqueIdentifier;
@@ -54,6 +55,7 @@ function initModules() {
5455
useRef = React.useRef;
5556
useDebugValue = React.useDebugValue;
5657
useImperativeHandle = React.useImperativeHandle;
58+
useInsertionEffect = React.unstable_useInsertionEffect;
5759
useLayoutEffect = React.useLayoutEffect;
5860
useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
5961
forwardRef = React.forwardRef;
@@ -638,6 +640,22 @@ describe('ReactDOMServerHooks', () => {
638640
expect(domNode.textContent).toEqual('Count: 0');
639641
});
640642
});
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+
});
641659

642660
describe('useLayoutEffect', () => {
643661
it('should warn when invoked during render', async () => {

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

+17
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,22 @@ function useRef<T>(initialValue: T): {|current: T|} {
385385
}
386386
}
387387

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+
388404
export function useLayoutEffect(
389405
create: () => (() => void) | void,
390406
inputs: Array<mixed> | void | null,
@@ -508,6 +524,7 @@ export const Dispatcher: DispatcherType = {
508524
useReducer,
509525
useRef,
510526
useState,
527+
useInsertionEffect,
511528
useLayoutEffect,
512529
useCallback,
513530
// useImperativeHandle is not run in the server environment

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ import {
136136
NoFlags as NoHookEffect,
137137
HasEffect as HookHasEffect,
138138
Layout as HookLayout,
139+
Insertion as HookInsertion,
139140
Passive as HookPassive,
140141
} from './ReactHookEffectTags';
141142
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
@@ -525,6 +526,8 @@ function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
525526
let hookName;
526527
if ((effect.tag & HookLayout) !== NoFlags) {
527528
hookName = 'useLayoutEffect';
529+
} else if ((effect.tag & HookInsertion) !== NoFlags) {
530+
hookName = 'useInsertionEffect';
528531
} else {
529532
hookName = 'useEffect';
530533
}
@@ -1153,7 +1156,10 @@ function commitUnmount(
11531156
do {
11541157
const {destroy, tag} = effect;
11551158
if (destroy !== undefined) {
1156-
if ((tag & HookLayout) !== NoHookEffect) {
1159+
if (
1160+
(tag & HookInsertion) !== NoHookEffect ||
1161+
(tag & HookLayout) !== NoHookEffect
1162+
) {
11571163
if (
11581164
enableProfilerTimer &&
11591165
enableProfilerCommitHooks &&
@@ -1738,6 +1744,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
17381744
case ForwardRef:
17391745
case MemoComponent:
17401746
case SimpleMemoComponent: {
1747+
commitHookEffectListUnmount(
1748+
HookInsertion | HookHasEffect,
1749+
finishedWork,
1750+
finishedWork.return,
1751+
);
1752+
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
1753+
17411754
// Layout effects are destroyed during the mutation phase so that all
17421755
// destroy functions for all fibers are called before any create functions.
17431756
// This prevents sibling component effects from interfering with each other,
@@ -1810,6 +1823,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
18101823
case ForwardRef:
18111824
case MemoComponent:
18121825
case SimpleMemoComponent: {
1826+
commitHookEffectListUnmount(
1827+
HookInsertion | HookHasEffect,
1828+
finishedWork,
1829+
finishedWork.return,
1830+
);
1831+
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
18131832
// Layout effects are destroyed during the mutation phase so that all
18141833
// destroy functions for all fibers are called before any create functions.
18151834
// This prevents sibling component effects from interfering with each other,

0 commit comments

Comments
 (0)