Skip to content

Commit a10210c

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

21 files changed

+812
-6
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: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,113 @@ describe('ReactHooksInspection', () => {
237237
]);
238238
});
239239

240+
// @gate experimental || www
241+
// @gate source // TODO: Build gets confused by the unstable prefix.
242+
it('should inspect a tree of multiple levels of hooks, including useMutationEffect', () => {
243+
function effect() {}
244+
function useCustom(value) {
245+
const [state] = React.useReducer((s, a) => s, value);
246+
React.useEffect(effect);
247+
return state;
248+
}
249+
function useBar(value) {
250+
const result = useCustom(value);
251+
React.useLayoutEffect(effect);
252+
return result;
253+
}
254+
function useBaz(value) {
255+
React.unstable_useMutationEffect(effect);
256+
const result = useCustom(value);
257+
return result;
258+
}
259+
function Foo(props) {
260+
const value1 = useBar('hello');
261+
const value2 = useBaz('world');
262+
return (
263+
<div>
264+
{value1} {value2}
265+
</div>
266+
);
267+
}
268+
const tree = ReactDebugTools.inspectHooks(Foo, {});
269+
expect(tree).toEqual([
270+
{
271+
isStateEditable: false,
272+
id: null,
273+
name: 'Bar',
274+
value: undefined,
275+
subHooks: [
276+
{
277+
isStateEditable: false,
278+
id: null,
279+
name: 'Custom',
280+
value: undefined,
281+
subHooks: [
282+
{
283+
isStateEditable: true,
284+
id: 0,
285+
name: 'Reducer',
286+
value: 'hello',
287+
subHooks: [],
288+
},
289+
{
290+
isStateEditable: false,
291+
id: 1,
292+
name: 'Effect',
293+
value: effect,
294+
subHooks: [],
295+
},
296+
],
297+
},
298+
{
299+
isStateEditable: false,
300+
id: 2,
301+
name: 'LayoutEffect',
302+
value: effect,
303+
subHooks: [],
304+
},
305+
],
306+
},
307+
{
308+
isStateEditable: false,
309+
id: null,
310+
name: 'Baz',
311+
value: undefined,
312+
subHooks: [
313+
{
314+
isStateEditable: false,
315+
id: 3,
316+
name: 'MutationEffect',
317+
value: effect,
318+
subHooks: [],
319+
},
320+
{
321+
isStateEditable: false,
322+
id: null,
323+
name: 'Custom',
324+
subHooks: [
325+
{
326+
isStateEditable: true,
327+
id: 4,
328+
name: 'Reducer',
329+
subHooks: [],
330+
value: 'world',
331+
},
332+
{
333+
isStateEditable: false,
334+
id: 5,
335+
name: 'Effect',
336+
subHooks: [],
337+
value: effect,
338+
},
339+
],
340+
value: undefined,
341+
},
342+
],
343+
},
344+
]);
345+
});
346+
240347
it('should inspect the default value using the useContext hook', () => {
241348
const MyContext = React.createContext('default');
242349
function Foo(props) {

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

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

271+
// @gate experimental || www
272+
// @gate source // TODO: Build gets confused by the unstable prefix.
273+
it('should inspect the current state of all stateful hooks, including useMutationEffect', () => {
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+
React.unstable_useMutationEffect(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: 'MutationEffect',
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: 'MutationEffect',
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

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 || www
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 () => {

0 commit comments

Comments
 (0)