[Experimental] Add useInsertionEffect (#21913)

This commit is contained in:
Ricky
2021-09-14 10:27:09 -04:00
committed by GitHub
parent 806aaa2e21
commit 263cfa6ecb
20 changed files with 862 additions and 6 deletions
+14
View File
@@ -78,6 +78,7 @@ function getPrimitiveStackCache(): Map<string, Array<any>> {
Dispatcher.useCacheRefresh();
}
Dispatcher.useLayoutEffect(() => {});
Dispatcher.useInsertionEffect(() => {});
Dispatcher.useEffect(() => {});
Dispatcher.useImperativeHandle(undefined, () => null);
Dispatcher.useDebugValue(null);
@@ -191,6 +192,18 @@ function useLayoutEffect(
});
}
function useInsertionEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
): void {
nextHook();
hookLog.push({
primitive: 'InsertionEffect',
stackError: new Error(),
value: create,
});
}
function useEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
@@ -338,6 +351,7 @@ const Dispatcher: DispatcherType = {
useImperativeHandle,
useDebugValue,
useLayoutEffect,
useInsertionEffect,
useMemo,
useReducer,
useRef,
@@ -268,6 +268,183 @@ describe('ReactHooksInspectionIntegration', () => {
]);
});
// @gate experimental || www
it('should inspect the current state of all stateful hooks, including useInsertionEffect', () => {
const useInsertionEffect = React.unstable_useInsertionEffect;
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');
useInsertionEffect(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: 'InsertionEffect',
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: 'InsertionEffect',
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) {
@@ -27,6 +27,7 @@ let useCallback;
let useMemo;
let useRef;
let useImperativeHandle;
let useInsertionEffect;
let useLayoutEffect;
let useDebugValue;
let useOpaqueIdentifier;
@@ -54,6 +55,7 @@ function initModules() {
useRef = React.useRef;
useDebugValue = React.useDebugValue;
useImperativeHandle = React.useImperativeHandle;
useInsertionEffect = React.unstable_useInsertionEffect;
useLayoutEffect = React.useLayoutEffect;
useOpaqueIdentifier = React.unstable_useOpaqueIdentifier;
forwardRef = React.forwardRef;
@@ -638,6 +640,22 @@ describe('ReactDOMServerHooks', () => {
expect(domNode.textContent).toEqual('Count: 0');
});
});
describe('useInsertionEffect', () => {
// @gate experimental || www
it('should warn when invoked during render', async () => {
function Counter() {
useInsertionEffect(() => {
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 () => {
@@ -385,6 +385,22 @@ function useRef<T>(initialValue: T): {|current: T|} {
}
}
function useInsertionEffect(
create: () => mixed,
inputs: Array<mixed> | void | null,
) {
if (__DEV__) {
currentHookNameInDev = 'useInsertionEffect';
console.error(
'useInsertionEffect 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, useInsertionEffect should only be used in ' +
'components that render exclusively on the client.',
);
}
}
export function useLayoutEffect(
create: () => (() => void) | void,
inputs: Array<mixed> | void | null,
@@ -508,6 +524,7 @@ export const Dispatcher: DispatcherType = {
useReducer,
useRef,
useState,
useInsertionEffect,
useLayoutEffect,
useCallback,
// useImperativeHandle is not run in the server environment
@@ -136,6 +136,7 @@ import {
NoFlags as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Insertion as HookInsertion,
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
@@ -525,6 +526,8 @@ function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
let hookName;
if ((effect.tag & HookLayout) !== NoFlags) {
hookName = 'useLayoutEffect';
} else if ((effect.tag & HookInsertion) !== NoFlags) {
hookName = 'useInsertionEffect';
} else {
hookName = 'useEffect';
}
@@ -1153,7 +1156,10 @@ function commitUnmount(
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookLayout) !== NoHookEffect) {
if (
(tag & HookInsertion) !== NoHookEffect ||
(tag & HookLayout) !== NoHookEffect
) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
@@ -1738,6 +1744,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
@@ -1810,6 +1823,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
@@ -136,6 +136,7 @@ import {
NoFlags as NoHookEffect,
HasEffect as HookHasEffect,
Layout as HookLayout,
Insertion as HookInsertion,
Passive as HookPassive,
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.old';
@@ -525,6 +526,8 @@ function commitHookEffectListMount(tag: HookFlags, finishedWork: Fiber) {
let hookName;
if ((effect.tag & HookLayout) !== NoFlags) {
hookName = 'useLayoutEffect';
} else if ((effect.tag & HookInsertion) !== NoFlags) {
hookName = 'useInsertionEffect';
} else {
hookName = 'useEffect';
}
@@ -1153,7 +1156,10 @@ function commitUnmount(
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookLayout) !== NoHookEffect) {
if (
(tag & HookInsertion) !== NoHookEffect ||
(tag & HookLayout) !== NoHookEffect
) {
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
@@ -1738,6 +1744,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
@@ -1810,6 +1823,12 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// Layout effects are destroyed during the mutation phase so that all
// destroy functions for all fibers are called before any create functions.
// This prevents sibling component effects from interfering with each other,
@@ -75,6 +75,7 @@ import {
HasEffect as HookHasEffect,
Layout as HookLayout,
Passive as HookPassive,
Insertion as HookInsertion,
} from './ReactHookEffectTags';
import {
getWorkInProgressRoot,
@@ -1708,6 +1709,20 @@ function updateEffect(
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function mountInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
function updateInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2310,6 +2325,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useInsertionEffect: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
@@ -2337,6 +2353,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
@@ -2362,6 +2379,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
@@ -2388,6 +2406,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: rerenderReducer,
@@ -2468,6 +2487,15 @@ if (__DEV__) {
checkDepsAreArrayDev(deps);
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2602,6 +2630,14 @@ if (__DEV__) {
updateHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2734,6 +2770,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2867,6 +2911,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3004,6 +3056,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3152,6 +3213,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3301,6 +3371,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -75,6 +75,7 @@ import {
HasEffect as HookHasEffect,
Layout as HookLayout,
Passive as HookPassive,
Insertion as HookInsertion,
} from './ReactHookEffectTags';
import {
getWorkInProgressRoot,
@@ -1708,6 +1709,20 @@ function updateEffect(
return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
}
function mountInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return mountEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
function updateInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
return updateEffectImpl(UpdateEffect, HookInsertion, create, deps);
}
function mountLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2310,6 +2325,7 @@ export const ContextOnlyDispatcher: Dispatcher = {
useContext: throwInvalidHookError,
useEffect: throwInvalidHookError,
useImperativeHandle: throwInvalidHookError,
useInsertionEffect: throwInvalidHookError,
useLayoutEffect: throwInvalidHookError,
useMemo: throwInvalidHookError,
useReducer: throwInvalidHookError,
@@ -2337,6 +2353,7 @@ const HooksDispatcherOnMount: Dispatcher = {
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
@@ -2362,6 +2379,7 @@ const HooksDispatcherOnUpdate: Dispatcher = {
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
@@ -2388,6 +2406,7 @@ const HooksDispatcherOnRerender: Dispatcher = {
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: rerenderReducer,
@@ -2468,6 +2487,15 @@ if (__DEV__) {
checkDepsAreArrayDev(deps);
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
mountHookTypesDev();
checkDepsAreArrayDev(deps);
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2602,6 +2630,14 @@ if (__DEV__) {
updateHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2734,6 +2770,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -2867,6 +2911,14 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3004,6 +3056,15 @@ if (__DEV__) {
mountHookTypesDev();
return mountImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
mountHookTypesDev();
return mountInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3152,6 +3213,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -3301,6 +3371,15 @@ if (__DEV__) {
updateHookTypesDev();
return updateImperativeHandle(ref, create, deps);
},
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
currentHookNameInDev = 'useInsertionEffect';
warnInvalidHookAccess();
updateHookTypesDev();
return updateInsertionEffect(create, deps);
},
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
+5 -4
View File
@@ -9,11 +9,12 @@
export type HookFlags = number;
export const NoFlags = /* */ 0b000;
export const NoFlags = /* */ 0b0000;
// Represents whether effect should fire.
export const HasEffect = /* */ 0b001;
export const HasEffect = /* */ 0b0001;
// Represents the phase in which the effect (not the clean-up) fires.
export const Layout = /* */ 0b010;
export const Passive = /* */ 0b100;
export const Insertion = /* */ 0b0010;
export const Layout = /* */ 0b0100;
export const Passive = /* */ 0b1000;
+5
View File
@@ -33,6 +33,7 @@ export type HookType =
| 'useContext'
| 'useRef'
| 'useEffect'
| 'useInsertionEffect'
| 'useLayoutEffect'
| 'useCallback'
| 'useMemo'
@@ -286,6 +287,10 @@ export type Dispatcher = {|
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void,
useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
@@ -22,6 +22,7 @@ let Suspense;
let useState;
let useReducer;
let useEffect;
let useInsertionEffect;
let useLayoutEffect;
let useCallback;
let useMemo;
@@ -46,6 +47,7 @@ describe('ReactHooksWithNoopRenderer', () => {
useState = React.useState;
useReducer = React.useReducer;
useEffect = React.useEffect;
useInsertionEffect = React.unstable_useInsertionEffect;
useLayoutEffect = React.useLayoutEffect;
useCallback = React.useCallback;
useMemo = React.useMemo;
@@ -2682,6 +2684,415 @@ describe('ReactHooksWithNoopRenderer', () => {
});
});
describe('useInsertionEffect', () => {
// @gate experimental || www
it('fires insertion effects after snapshots on update', () => {
function CounterA(props) {
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(`Create insertion`);
return () => {
Scheduler.unstable_yieldValue(`Destroy insertion`);
};
});
return null;
}
class CounterB extends React.Component {
getSnapshotBeforeUpdate(prevProps, prevState) {
Scheduler.unstable_yieldValue(`Get Snapshot`);
return null;
}
componentDidUpdate() {}
render() {
return null;
}
}
act(() => {
ReactNoop.render(
<>
<CounterA />
<CounterB />
</>,
);
expect(Scheduler).toFlushAndYield(['Create insertion']);
});
// Update
act(() => {
ReactNoop.render(
<>
<CounterA />
<CounterB />
</>,
);
expect(Scheduler).toFlushAndYield([
'Get Snapshot',
'Destroy insertion',
'Create insertion',
]);
});
// Unmount everything
act(() => {
ReactNoop.render(null);
expect(Scheduler).toFlushAndYield(['Destroy insertion']);
});
});
// @gate experimental || www
it('fires insertion effects before layout effects', () => {
let committedText = '(empty)';
function Counter(props) {
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create insertion [current: ${committedText}]`,
);
committedText = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy insertion [current: ${committedText}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create layout [current: ${committedText}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy layout [current: ${committedText}]`,
);
};
});
useEffect(() => {
Scheduler.unstable_yieldValue(
`Create passive [current: ${committedText}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy passive [current: ${committedText}]`,
);
};
});
return null;
}
act(() => {
ReactNoop.render(<Counter count={0} />);
expect(Scheduler).toFlushUntilNextPaint([
'Create insertion [current: (empty)]',
'Create layout [current: 0]',
]);
expect(committedText).toEqual('0');
});
expect(Scheduler).toHaveYielded(['Create passive [current: 0]']);
// Unmount everything
act(() => {
ReactNoop.render(null);
expect(Scheduler).toFlushUntilNextPaint([
'Destroy insertion [current: 0]',
'Destroy layout [current: 0]',
]);
});
expect(Scheduler).toHaveYielded(['Destroy passive [current: 0]']);
});
// @gate experimental || www
it('force flushes passive effects before firing new insertion effects', () => {
let committedText = '(empty)';
function Counter(props) {
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create insertion [current: ${committedText}]`,
);
committedText = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy insertion [current: ${committedText}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create layout [current: ${committedText}]`,
);
committedText = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy layout [current: ${committedText}]`,
);
};
});
useEffect(() => {
Scheduler.unstable_yieldValue(
`Create passive [current: ${committedText}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy passive [current: ${committedText}]`,
);
};
});
return null;
}
act(() => {
React.startTransition(() => {
ReactNoop.render(<Counter count={0} />);
});
expect(Scheduler).toFlushUntilNextPaint([
'Create insertion [current: (empty)]',
'Create layout [current: 0]',
]);
expect(committedText).toEqual('0');
React.startTransition(() => {
ReactNoop.render(<Counter count={1} />);
});
expect(Scheduler).toFlushUntilNextPaint([
'Create passive [current: 0]',
'Destroy insertion [current: 0]',
'Create insertion [current: 0]',
'Destroy layout [current: 1]',
'Create layout [current: 1]',
]);
expect(committedText).toEqual('1');
});
expect(Scheduler).toHaveYielded([
'Destroy passive [current: 1]',
'Create passive [current: 1]',
]);
});
// @gate experimental || www
it('fires all insertion effects (interleaved) before firing any layout effects', () => {
let committedA = '(empty)';
let committedB = '(empty)';
function CounterA(props) {
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);
committedA = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy Insertion 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);
};
});
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);
committedA = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy Insertion 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy Layout 1 for Component A [A: ${committedA}, B: ${committedB}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy Layout 2 for Component A [A: ${committedA}, B: ${committedB}]`,
);
};
});
return null;
}
function CounterB(props) {
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);
committedB = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy Insertion 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);
};
});
useInsertionEffect(() => {
Scheduler.unstable_yieldValue(
`Create Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);
committedB = props.count + '';
return () => {
Scheduler.unstable_yieldValue(
`Destroy Insertion 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy Layout 1 for Component B [A: ${committedA}, B: ${committedB}]`,
);
};
});
useLayoutEffect(() => {
Scheduler.unstable_yieldValue(
`Create Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);
return () => {
Scheduler.unstable_yieldValue(
`Destroy Layout 2 for Component B [A: ${committedA}, B: ${committedB}]`,
);
};
});
return null;
}
act(() => {
ReactNoop.render(
<React.Fragment>
<CounterA count={0} />
<CounterB count={0} />
</React.Fragment>,
);
expect(Scheduler).toFlushAndYield([
// All insertion effects fire before all layout effects
'Create Insertion 1 for Component A [A: (empty), B: (empty)]',
'Create Insertion 2 for Component A [A: 0, B: (empty)]',
'Create Insertion 1 for Component B [A: 0, B: (empty)]',
'Create Insertion 2 for Component B [A: 0, B: 0]',
'Create Layout 1 for Component A [A: 0, B: 0]',
'Create Layout 2 for Component A [A: 0, B: 0]',
'Create Layout 1 for Component B [A: 0, B: 0]',
'Create Layout 2 for Component B [A: 0, B: 0]',
]);
expect([committedA, committedB]).toEqual(['0', '0']);
});
act(() => {
ReactNoop.render(
<React.Fragment>
<CounterA count={1} />
<CounterB count={1} />
</React.Fragment>,
);
expect(Scheduler).toFlushAndYield([
'Destroy Insertion 1 for Component A [A: 0, B: 0]',
'Destroy Insertion 2 for Component A [A: 0, B: 0]',
'Create Insertion 1 for Component A [A: 0, B: 0]',
'Create Insertion 2 for Component A [A: 1, B: 0]',
'Destroy Layout 1 for Component A [A: 1, B: 0]',
'Destroy Layout 2 for Component A [A: 1, B: 0]',
'Destroy Insertion 1 for Component B [A: 1, B: 0]',
'Destroy Insertion 2 for Component B [A: 1, B: 0]',
'Create Insertion 1 for Component B [A: 1, B: 0]',
'Create Insertion 2 for Component B [A: 1, B: 1]',
'Destroy Layout 1 for Component B [A: 1, B: 1]',
'Destroy Layout 2 for Component B [A: 1, B: 1]',
'Create Layout 1 for Component A [A: 1, B: 1]',
'Create Layout 2 for Component A [A: 1, B: 1]',
'Create Layout 1 for Component B [A: 1, B: 1]',
'Create Layout 2 for Component B [A: 1, B: 1]',
]);
expect([committedA, committedB]).toEqual(['1', '1']);
// Unmount everything
act(() => {
ReactNoop.render(null);
expect(Scheduler).toFlushAndYield([
'Destroy Insertion 1 for Component A [A: 1, B: 1]',
'Destroy Insertion 2 for Component A [A: 1, B: 1]',
'Destroy Layout 1 for Component A [A: 1, B: 1]',
'Destroy Layout 2 for Component A [A: 1, B: 1]',
'Destroy Insertion 1 for Component B [A: 1, B: 1]',
'Destroy Insertion 2 for Component B [A: 1, B: 1]',
'Destroy Layout 1 for Component B [A: 1, B: 1]',
'Destroy Layout 2 for Component B [A: 1, B: 1]',
]);
});
});
});
// @gate experimental || www
it('assumes insertion effect destroy function is either a function or undefined', () => {
function App(props) {
useInsertionEffect(() => {
return props.return;
});
return null;
}
const root1 = ReactNoop.createRoot();
expect(() =>
act(() => {
root1.render(<App return={17} />);
}),
).toErrorDev([
'Warning: useInsertionEffect must not return anything besides a ' +
'function, which is used for clean-up. You returned: 17',
]);
const root2 = ReactNoop.createRoot();
expect(() =>
act(() => {
root2.render(<App return={null} />);
}),
).toErrorDev([
'Warning: useInsertionEffect must not return anything besides a ' +
'function, which is used for clean-up. You returned null. If your ' +
'effect does not require clean up, return undefined (or nothing).',
]);
const root3 = ReactNoop.createRoot();
expect(() =>
act(() => {
root3.render(<App return={Promise.resolve()} />);
}),
).toErrorDev([
'Warning: useInsertionEffect must not return anything besides a ' +
'function, which is used for clean-up.\n\n' +
'It looks like you wrote useInsertionEffect(async () => ...) or returned a Promise.',
]);
// Error on unmount because React assumes the value is a function
expect(() =>
act(() => {
root3.unmount();
}),
).toThrow('is not a function');
});
});
describe('useLayoutEffect', () => {
it('fires layout effects after the host has been mutated', () => {
function getCommittedText() {
+1
View File
@@ -503,6 +503,7 @@ export const Dispatcher: DispatcherType = {
useReducer,
useRef,
useState,
useInsertionEffect: noop,
useLayoutEffect,
useCallback,
// useImperativeHandle is not run in the server environment
+1
View File
@@ -822,6 +822,7 @@ const Dispatcher: DispatcherType = {
useReducer: (unsupportedHook: any),
useRef: (unsupportedHook: any),
useState: (unsupportedHook: any),
useInsertionEffect: (unsupportedHook: any),
useLayoutEffect: (unsupportedHook: any),
useImperativeHandle: (unsupportedHook: any),
useEffect: (unsupportedHook: any),
@@ -35,6 +35,7 @@ export function waitForSuspense<T>(fn: () => T): Promise<T> {
useReducer: unsupported,
useRef: unsupported,
useState: unsupported,
useInsertionEffect: unsupported,
useLayoutEffect: unsupported,
useCallback: unsupported,
useImperativeHandle: unsupported,
+1
View File
@@ -48,6 +48,7 @@ export {
useEffect,
useImperativeHandle,
useLayoutEffect,
unstable_useInsertionEffect,
useMemo,
useMutableSource,
useMutableSource as unstable_useMutableSource,
+1
View File
@@ -42,6 +42,7 @@ export {
useDeferredValue,
useEffect,
useImperativeHandle,
unstable_useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource as unstable_useMutableSource,
+1
View File
@@ -67,6 +67,7 @@ export {
useDeferredValue,
useEffect,
useImperativeHandle,
unstable_useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource,
+1
View File
@@ -46,6 +46,7 @@ export {
useDeferredValue as unstable_useDeferredValue, // TODO: Remove once call sights updated to useDeferredValue
useEffect,
useImperativeHandle,
unstable_useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource,
+2
View File
@@ -41,6 +41,7 @@ import {
useEffect,
useImperativeHandle,
useDebugValue,
useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource,
@@ -91,6 +92,7 @@ export {
useEffect,
useImperativeHandle,
useDebugValue,
useInsertionEffect as unstable_useInsertionEffect,
useLayoutEffect,
useMemo,
useMutableSource,
+8
View File
@@ -100,6 +100,14 @@ export function useEffect(
return dispatcher.useEffect(create, deps);
}
export function useInsertionEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,
): void {
const dispatcher = resolveDispatcher();
return dispatcher.useInsertionEffect(create, deps);
}
export function useLayoutEffect(
create: () => (() => void) | void,
deps: Array<mixed> | void | null,