Skip to content

Commit 2f933e4

Browse files
committed
[Experimental] Add back useMutationEffect
1 parent 9b76d2d commit 2f933e4

18 files changed

+590
-7
lines changed

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

Lines changed: 14 additions & 0 deletions
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.useMutationEffect(() => {});
8182
Dispatcher.useEffect(() => {});
8283
Dispatcher.useImperativeHandle(undefined, () => null);
8384
Dispatcher.useDebugValue(null);
@@ -191,6 +192,18 @@ function useLayoutEffect(
191192
});
192193
}
193194

195+
function useMutationEffect(
196+
create: () => mixed,
197+
inputs: Array<mixed> | void | null,
198+
): void {
199+
nextHook();
200+
hookLog.push({
201+
primitive: 'MutationEffect',
202+
stackError: new Error(),
203+
value: create,
204+
});
205+
}
206+
194207
function useEffect(
195208
create: () => (() => void) | void,
196209
inputs: Array<mixed> | void | null,
@@ -320,6 +333,7 @@ const Dispatcher: DispatcherType = {
320333
useImperativeHandle,
321334
useDebugValue,
322335
useLayoutEffect,
336+
useMutationEffect,
323337
useMemo,
324338
useReducer,
325339
useRef,

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,7 @@ describe('ReactHooksInspection', () => {
132132
]);
133133
});
134134

135+
// @gate experimental
135136
it('should inspect a tree of multiple levels of hooks', () => {
136137
function effect() {}
137138
function useCustom(value) {
@@ -145,7 +146,7 @@ describe('ReactHooksInspection', () => {
145146
return result;
146147
}
147148
function useBaz(value) {
148-
React.useLayoutEffect(effect);
149+
React.unstable_useMutationEffect(effect);
149150
const result = useCustom(value);
150151
return result;
151152
}
@@ -206,7 +207,7 @@ describe('ReactHooksInspection', () => {
206207
{
207208
isStateEditable: false,
208209
id: 3,
209-
name: 'LayoutEffect',
210+
name: 'MutationEffect',
210211
value: effect,
211212
subHooks: [],
212213
},

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

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,182 @@ describe('ReactHooksInspectionIntegration', () => {
268268
]);
269269
});
270270

271+
// @gate experimental
272+
it('should inspect the current state of all stateful hooks, including useMutationEffect', () => {
273+
const outsideRef = React.createRef();
274+
function effect() {}
275+
function Foo(props) {
276+
const [state1, setState] = React.useState('a');
277+
const [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
278+
const ref = React.useRef('c');
279+
280+
React.unstable_useMutationEffect(effect);
281+
React.useLayoutEffect(effect);
282+
React.useEffect(effect);
283+
284+
React.useImperativeHandle(
285+
outsideRef,
286+
() => {
287+
// Return a function so that jest treats them as non-equal.
288+
return function Instance() {};
289+
},
290+
[],
291+
);
292+
293+
React.useMemo(() => state1 + state2, [state1]);
294+
295+
function update() {
296+
act(() => {
297+
setState('A');
298+
});
299+
act(() => {
300+
dispatch({value: 'B'});
301+
});
302+
ref.current = 'C';
303+
}
304+
const memoizedUpdate = React.useCallback(update, []);
305+
return (
306+
<div onClick={memoizedUpdate}>
307+
{state1} {state2}
308+
</div>
309+
);
310+
}
311+
let renderer;
312+
act(() => {
313+
renderer = ReactTestRenderer.create(<Foo prop="prop" />);
314+
});
315+
316+
let childFiber = renderer.root.findByType(Foo)._currentFiber();
317+
318+
const {onClick: updateStates} = renderer.root.findByType('div').props;
319+
320+
let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
321+
expect(tree).toEqual([
322+
{
323+
isStateEditable: true,
324+
id: 0,
325+
name: 'State',
326+
value: 'a',
327+
subHooks: [],
328+
},
329+
{
330+
isStateEditable: true,
331+
id: 1,
332+
name: 'Reducer',
333+
value: 'b',
334+
subHooks: [],
335+
},
336+
{isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
337+
{
338+
isStateEditable: false,
339+
id: 3,
340+
name: 'MutationEffect',
341+
value: effect,
342+
subHooks: [],
343+
},
344+
{
345+
isStateEditable: false,
346+
id: 4,
347+
name: 'LayoutEffect',
348+
value: effect,
349+
subHooks: [],
350+
},
351+
{
352+
isStateEditable: false,
353+
id: 5,
354+
name: 'Effect',
355+
value: effect,
356+
subHooks: [],
357+
},
358+
{
359+
isStateEditable: false,
360+
id: 6,
361+
name: 'ImperativeHandle',
362+
value: outsideRef.current,
363+
subHooks: [],
364+
},
365+
{
366+
isStateEditable: false,
367+
id: 7,
368+
name: 'Memo',
369+
value: 'ab',
370+
subHooks: [],
371+
},
372+
{
373+
isStateEditable: false,
374+
id: 8,
375+
name: 'Callback',
376+
value: updateStates,
377+
subHooks: [],
378+
},
379+
]);
380+
381+
updateStates();
382+
383+
childFiber = renderer.root.findByType(Foo)._currentFiber();
384+
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
385+
386+
expect(tree).toEqual([
387+
{
388+
isStateEditable: true,
389+
id: 0,
390+
name: 'State',
391+
value: 'A',
392+
subHooks: [],
393+
},
394+
{
395+
isStateEditable: true,
396+
id: 1,
397+
name: 'Reducer',
398+
value: 'B',
399+
subHooks: [],
400+
},
401+
{isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
402+
{
403+
isStateEditable: false,
404+
id: 3,
405+
name: 'MutationEffect',
406+
value: effect,
407+
subHooks: [],
408+
},
409+
{
410+
isStateEditable: false,
411+
id: 4,
412+
name: 'LayoutEffect',
413+
value: effect,
414+
subHooks: [],
415+
},
416+
{
417+
isStateEditable: false,
418+
id: 5,
419+
name: 'Effect',
420+
value: effect,
421+
subHooks: [],
422+
},
423+
{
424+
isStateEditable: false,
425+
id: 6,
426+
name: 'ImperativeHandle',
427+
value: outsideRef.current,
428+
subHooks: [],
429+
},
430+
{
431+
isStateEditable: false,
432+
id: 7,
433+
name: 'Memo',
434+
value: 'Ab',
435+
subHooks: [],
436+
},
437+
{
438+
isStateEditable: false,
439+
id: 8,
440+
name: 'Callback',
441+
value: updateStates,
442+
subHooks: [],
443+
},
444+
]);
445+
});
446+
271447
it('should inspect the value of the current provider in useContext', () => {
272448
const MyContext = React.createContext('default');
273449
function Foo(props) {

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

Lines changed: 18 additions & 0 deletions
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 useMutationEffect;
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+
useMutationEffect = React.unstable_useMutationEffect;
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('useMutationEffect', () => {
644+
// @gate experimental
645+
it('should warn when invoked during render', async () => {
646+
function Counter() {
647+
useMutationEffect(() => {
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

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

388+
function useMutationEffect(
389+
create: () => mixed,
390+
inputs: Array<mixed> | void | null,
391+
) {
392+
if (__DEV__) {
393+
currentHookNameInDev = 'useMutationEffect';
394+
console.error(
395+
'useMutationEffect 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, useMutationEffect 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,
@@ -501,6 +517,7 @@ export const Dispatcher: DispatcherType = {
501517
useReducer,
502518
useRef,
503519
useState,
520+
useMutationEffect,
504521
useLayoutEffect,
505522
useCallback,
506523
// useImperativeHandle is not run in the server environment

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

Lines changed: 32 additions & 1 deletion
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+
Mutation as HookMutation,
139140
Passive as HookPassive,
140141
} from './ReactHookEffectTags';
141142
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
@@ -1143,7 +1144,10 @@ function commitUnmount(
11431144
do {
11441145
const {destroy, tag} = effect;
11451146
if (destroy !== undefined) {
1146-
if ((tag & HookLayout) !== NoHookEffect) {
1147+
if (
1148+
(tag & HookMutation) !== NoHookEffect ||
1149+
(tag & HookLayout) !== NoHookEffect
1150+
) {
11471151
if (
11481152
enableProfilerTimer &&
11491153
enableProfilerCommitHooks &&
@@ -1745,6 +1749,15 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
17451749
) {
17461750
try {
17471751
startLayoutEffectTimer();
1752+
commitHookEffectListUnmount(
1753+
HookMutation | HookHasEffect,
1754+
finishedWork,
1755+
finishedWork.return,
1756+
);
1757+
commitHookEffectListMount(
1758+
HookMutation | HookHasEffect,
1759+
finishedWork,
1760+
);
17481761
commitHookEffectListUnmount(
17491762
HookLayout | HookHasEffect,
17501763
finishedWork,
@@ -1754,6 +1767,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
17541767
recordLayoutEffectDuration(finishedWork);
17551768
}
17561769
} else {
1770+
commitHookEffectListUnmount(
1771+
HookMutation | HookHasEffect,
1772+
finishedWork,
1773+
finishedWork.return,
1774+
);
1775+
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
17571776
commitHookEffectListUnmount(
17581777
HookLayout | HookHasEffect,
17591778
finishedWork,
@@ -1812,6 +1831,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
18121831
) {
18131832
try {
18141833
startLayoutEffectTimer();
1834+
commitHookEffectListUnmount(
1835+
HookMutation | HookHasEffect,
1836+
finishedWork,
1837+
finishedWork.return,
1838+
);
1839+
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
18151840
commitHookEffectListUnmount(
18161841
HookLayout | HookHasEffect,
18171842
finishedWork,
@@ -1821,6 +1846,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
18211846
recordLayoutEffectDuration(finishedWork);
18221847
}
18231848
} else {
1849+
commitHookEffectListUnmount(
1850+
HookMutation | HookHasEffect,
1851+
finishedWork,
1852+
finishedWork.return,
1853+
);
1854+
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
18241855
commitHookEffectListUnmount(
18251856
HookLayout | HookHasEffect,
18261857
finishedWork,

0 commit comments

Comments
 (0)