Skip to content

Commit

Permalink
[Experimental] Add back useMutationEffect
Browse files Browse the repository at this point in the history
  • Loading branch information
rickhanlonii committed Jul 20, 2021
1 parent 9b76d2d commit c6ceff6
Show file tree
Hide file tree
Showing 20 changed files with 705 additions and 6 deletions.
14 changes: 14 additions & 0 deletions packages/react-debug-tools/src/ReactDebugHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useCacheRefresh();
}
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useMutationEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeHandle(undefined, () => null);
Dispatcher.useDebugValue(null);
Expand Down Expand Up @@ -191,6 +192,18 @@ function useLayoutEffect(
});
}

function useMutationEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({
primitive: 'MutationEffect',
stackError: new Error(),
value: create,
});
}

function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
Expand Down Expand Up @@ -320,6 +333,7 @@ const Dispatcher: DispatcherType = {
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useMutationEffect,
useMemo,
useReducer,
useRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,183 @@ describe('ReactHooksInspectionIntegration', () => {
]);
});

// @gate experimental || www
// @gate source // TODO: Build gets confused by the unstable prefix.
it('should inspect the current state of all stateful hooks, including useMutationEffect', () => {
const outsideRef = React.createRef();
function effect() {}
function Foo(props) {
const [state1, setState] = React.useState('a');
const [state2, dispatch] = React.useReducer((s, a) => a.value, 'b');
const ref = React.useRef('c');

React.unstable_useMutationEffect(effect);
React.useLayoutEffect(effect);
React.useEffect(effect);

React.useImperativeHandle(
outsideRef,
() => {
// Return a function so that jest treats them as non-equal.
return function Instance() {};
},
[],
);

React.useMemo(() => state1 + state2, [state1]);

function update() {
act(() => {
setState('A');
});
act(() => {
dispatch({value: 'B'});
});
ref.current = 'C';
}
const memoizedUpdate = React.useCallback(update, []);
return (
<div onClick={memoizedUpdate}>
{state1} {state2}
</div>
);
}
let renderer;
act(() => {
renderer = ReactTestRenderer.create(<Foo prop="prop" />);
});

let childFiber = renderer.root.findByType(Foo)._currentFiber();

const {onClick: updateStates} = renderer.root.findByType('div').props;

let tree = ReactDebugTools.inspectHooksOfFiber(childFiber);
expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'a',
subHooks: [],
},
{
isStateEditable: true,
id: 1,
name: 'Reducer',
value: 'b',
subHooks: [],
},
{isStateEditable: false, id: 2, name: 'Ref', value: 'c', subHooks: []},
{
isStateEditable: false,
id: 3,
name: 'MutationEffect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 4,
name: 'LayoutEffect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 5,
name: 'Effect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 6,
name: 'ImperativeHandle',
value: outsideRef.current,
subHooks: [],
},
{
isStateEditable: false,
id: 7,
name: 'Memo',
value: 'ab',
subHooks: [],
},
{
isStateEditable: false,
id: 8,
name: 'Callback',
value: updateStates,
subHooks: [],
},
]);

updateStates();

childFiber = renderer.root.findByType(Foo)._currentFiber();
tree = ReactDebugTools.inspectHooksOfFiber(childFiber);

expect(tree).toEqual([
{
isStateEditable: true,
id: 0,
name: 'State',
value: 'A',
subHooks: [],
},
{
isStateEditable: true,
id: 1,
name: 'Reducer',
value: 'B',
subHooks: [],
},
{isStateEditable: false, id: 2, name: 'Ref', value: 'C', subHooks: []},
{
isStateEditable: false,
id: 3,
name: 'MutationEffect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 4,
name: 'LayoutEffect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 5,
name: 'Effect',
value: effect,
subHooks: [],
},
{
isStateEditable: false,
id: 6,
name: 'ImperativeHandle',
value: outsideRef.current,
subHooks: [],
},
{
isStateEditable: false,
id: 7,
name: 'Memo',
value: 'Ab',
subHooks: [],
},
{
isStateEditable: false,
id: 8,
name: 'Callback',
value: updateStates,
subHooks: [],
},
]);
});

it('should inspect the value of the current provider in useContext', () => {
const MyContext = React.createContext('default');
function Foo(props) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ let useCallback;
let useMemo;
let useRef;
let useImperativeHandle;
let useMutationEffect;
let useLayoutEffect;
let useDebugValue;
let useOpaqueIdentifier;
Expand Down Expand Up @@ -54,6 +55,7 @@ function initModules() {
useRef = React.useRef;
useDebugValue = React.useDebugValue;
useImperativeHandle = React.useImperativeHandle;
useMutationEffect = React.unstable_useMutationEffect;
useLayoutEffect = React.useLayoutEffect;
useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
forwardRef = React.forwardRef;
Expand Down Expand Up @@ -638,6 +640,22 @@ describe('ReactDOMServerHooks', () => {
expect(domNode.textContent).toEqual('Count: 0');
});
});
describe('useMutationEffect', () => {
// @gate experimental || www
it('should warn when invoked during render', async () => {
function Counter() {
useMutationEffect(() => {
throw new Error('should not be invoked');
});

return <Text text="Count: 0" />;
}
const domNode = await serverRender(<Counter />, 1);
expect(clearYields()).toEqual(['Count: 0']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});
});

describe('useLayoutEffect', () => {
it('should warn when invoked during render', async () => {
Expand Down
17 changes: 17 additions & 0 deletions packages/react-dom/src/server/ReactPartialRendererHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,22 @@ function useRef<T>(initialValue: T): {|current: T|} {
}
}

function useMutationEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
) {
if (__DEV__) {
currentHookNameInDev = 'useMutationEffect';
console.error(
'useMutationEffect does nothing on the server, because its effect cannot ' +
"be encoded into the server renderer's output format. This will lead " +
'to a mismatch between the initial, non-hydrated UI and the intended ' +
'UI. To avoid this, useMutationEffect should only be used in ' +
'components that render exclusively on the client.',
);
}
}

export function useLayoutEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
Expand Down Expand Up @@ -501,6 +517,7 @@ export const Dispatcher: DispatcherType = {
useReducer,
useRef,
useState,
useMutationEffect,
useLayoutEffect,
useCallback,
// useImperativeHandle is not run in the server environment
Expand Down
33 changes: 32 additions & 1 deletion packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ import {
NoFlags as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Mutation as HookMutation,
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
Expand Down Expand Up @@ -1143,7 +1144,10 @@ function commitUnmount(
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookLayout) !== NoHookEffect) {
if (
(tag & HookMutation) !== NoHookEffect ||
(tag & HookLayout) !== NoHookEffect
) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
Expand Down Expand Up @@ -1745,6 +1749,15 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookMutation | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(
HookMutation | HookHasEffect,
finishedWork,
);
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
Expand All @@ -1754,6 +1767,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookMutation | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
Expand Down Expand Up @@ -1812,6 +1831,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
) {
try {
startLayoutEffectTimer();
commitHookEffectListUnmount(
HookMutation | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
Expand All @@ -1821,6 +1846,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
recordLayoutEffectDuration(finishedWork);
}
} else {
commitHookEffectListUnmount(
HookMutation | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookMutation | HookHasEffect, finishedWork);
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
Expand Down
Loading

0 comments on commit c6ceff6

Please sign in to comment.