/*** Copyright (c) Meta Platforms, Inc. and affiliates.** This source code is licensed under the MIT license found in the* LICENSE file in the root directory of this source tree.** @emails react-core* @jest-environment ./scripts/jest/ReactDOMServerIntegrationEnvironment*//* eslint-disable no-func-assign */'use strict';
const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils');
let React;
let ReactDOM;
let ReactDOMServer;
let ReactTestUtils;
let useState;
let useReducer;
let useEffect;
let useContext;
let useCallback;
let useMemo;
let useRef;
let useImperativeHandle;
let useInsertionEffect;
let useLayoutEffect;
let useDebugValue;
let forwardRef;
let yieldedValues;
let yieldValue;
let clearLog;
function initModules() {
// Reset warning cache.
jest.resetModules();
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
ReactTestUtils = require('react-dom/test-utils');
useState = React.useState;
useReducer = React.useReducer;
useEffect = React.useEffect;
useContext = React.useContext;
useCallback = React.useCallback;
useMemo = React.useMemo;
useRef = React.useRef;
useDebugValue = React.useDebugValue;
useImperativeHandle = React.useImperativeHandle;
useInsertionEffect = React.useInsertionEffect;
useLayoutEffect = React.useLayoutEffect;
forwardRef = React.forwardRef;
yieldedValues = [];
yieldValue = value => {
yieldedValues.push(value);
};clearLog = () => {
const ret = yieldedValues;
yieldedValues = [];
return ret;
};// Make them available to the helpers.
return {
ReactDOM,
ReactDOMServer,
ReactTestUtils,
};}const {resetModules, itRenders, itThrowsWhenRendering, serverRender} =
ReactDOMServerIntegrationUtils(initModules);
describe('ReactDOMServerHooks', () => {
beforeEach(() => {
resetModules();
});function Text(props) {
yieldValue(props.text);
return <span>{props.text}</span>;
}describe('useState', () => {itRenders('basic render', async render => {function Counter(props) {const [count] = useState(0);
return <span>Count: {count}</span>;}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('Count: 0');
});itRenders('lazy state initialization', async render => {function Counter(props) {const [count] = useState(() => {
return 0;});return <span>Count: {count}</span>;}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('Count: 0');
});it('does not trigger a re-renders when updater is invoked outside current render function', async () => {function UpdateCount({setCount, count, children}) {if (count < 3) {setCount(c => c + 1);
}return <span>{children}</span>;}function Counter() {const [count, setCount] = useState(0);
return (<div><UpdateCount setCount={setCount} count={count}>Count: {count}</UpdateCount></div>);}const domNode = await serverRender(<Counter />);expect(domNode.textContent).toEqual('Count: 0');
});itThrowsWhenRendering('if used inside a class component',async render => {class Counter extends React.Component {
render() {const [count] = useState(0);
return <Text text={count} />;}}return render(<Counter />);},'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);itRenders('multiple times when an updater is called', async render => {function Counter() {const [count, setCount] = useState(0);
if (count < 12) {setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);
}return <Text text={'Count: ' + count} />;
}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('Count: 12');
});itRenders('until there are no more new updates', async render => {function Counter() {const [count, setCount] = useState(0);
if (count < 3) {setCount(count + 1);
}return <span>Count: {count}</span>;}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('Count: 3');
});itThrowsWhenRendering('after too many iterations',async render => {function Counter() {const [count, setCount] = useState(0);
setCount(count + 1);
return <span>{count}</span>;}return render(<Counter />);},'Too many re-renders. React limits the number of renders to prevent ' +
'an infinite loop.',
);});describe('useReducer', () => {itRenders('with initial state', async render => {function reducer(state, action) {return action === 'increment' ? state + 1 : state;
}function Counter() {const [count] = useReducer(reducer, 0);
yieldValue('Render: ' + count);
return <Text text={count} />;}const domNode = await render(<Counter />);expect(clearLog()).toEqual(['Render: 0', 0]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('0');
});itRenders('lazy initialization', async render => {function reducer(state, action) {return action === 'increment' ? state + 1 : state;
}function Counter() {const [count] = useReducer(reducer, 0, c => c + 1);
yieldValue('Render: ' + count);
return <Text text={count} />;}const domNode = await render(<Counter />);expect(clearLog()).toEqual(['Render: 1', 1]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('1');
});itRenders('multiple times when updates happen during the render phase',async render => {function reducer(state, action) {return action === 'increment' ? state + 1 : state;
}function Counter() {const [count, dispatch] = useReducer(reducer, 0);
if (count < 3) {dispatch('increment');}yieldValue('Render: ' + count);
return <Text text={count} />;}const domNode = await render(<Counter />);expect(clearLog()).toEqual([
'Render: 0','Render: 1','Render: 2','Render: 3',3,]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('3');
},);itRenders('using reducer passed at time of render, not time of dispatch',async render => {// This test is a bit contrived but it demonstrates a subtle edge case.
// Reducer A increments by 1. Reducer B increments by 10.
function reducerA(state, action) {switch (action) {case 'increment':return state + 1;
case 'reset':return 0;}}function reducerB(state, action) {switch (action) {case 'increment':return state + 10;
case 'reset':return 0;}}function Counter() {const [reducer, setReducer] = useState(() => reducerA);
const [count, dispatch] = useReducer(reducer, 0);
if (count < 20) {dispatch('increment');// Swap reducers each time we incrementif (reducer === reducerA) {setReducer(() => reducerB);} else {setReducer(() => reducerA);}}yieldValue('Render: ' + count);
return <Text text={count} />;}const domNode = await render(<Counter />);expect(clearLog()).toEqual([
// The count should increase by alternating amounts of 10 and 1// until we reach 21.'Render: 0','Render: 10','Render: 11','Render: 21',21,]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('21');
},);});describe('useMemo', () => {itRenders('basic render', async render => {function CapitalizedText(props) {const text = props.text;
const capitalizedText = useMemo(() => {yieldValue(`Capitalize '${text}'`);
return text.toUpperCase();
}, [text]);
return <Text text={capitalizedText} />;}const domNode = await render(<CapitalizedText text="hello" />);expect(clearLog()).toEqual(["Capitalize 'hello'", 'HELLO']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('HELLO');
});itRenders('if no inputs are provided', async render => {function LazyCompute(props) {const computed = useMemo(props.compute);
return <Text text={computed} />;}function computeA() {yieldValue('compute A');return 'A';}const domNode = await render(<LazyCompute compute={computeA} />);expect(clearLog()).toEqual(['compute A', 'A']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('A');
});itRenders('multiple times when updates happen during the render phase',async render => {function CapitalizedText(props) {const [text, setText] = useState(props.text);
const capitalizedText = useMemo(() => {yieldValue(`Capitalize '${text}'`);
return text.toUpperCase();
}, [text]);
if (text === 'hello') {setText('hello, world.');
}return <Text text={capitalizedText} />;}const domNode = await render(<CapitalizedText text="hello" />);expect(clearLog()).toEqual([
"Capitalize 'hello'","Capitalize 'hello, world.'",'HELLO, WORLD.',]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('HELLO, WORLD.');
},);itRenders('should only invoke the memoized function when the inputs change',async render => {function CapitalizedText(props) {const [text, setText] = useState(props.text);
const [count, setCount] = useState(0);
const capitalizedText = useMemo(() => {yieldValue(`Capitalize '${text}'`);
return text.toUpperCase();
}, [text]);
yieldValue(count);if (count < 3) {setCount(count + 1);
}if (text === 'hello' && count === 2) {setText('hello, world.');
}return <Text text={capitalizedText} />;}const domNode = await render(<CapitalizedText text="hello" />);expect(clearLog()).toEqual([
"Capitalize 'hello'",0,1,2,// `capitalizedText` only recomputes when the text has changed"Capitalize 'hello, world.'",3,'HELLO, WORLD.',]);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('HELLO, WORLD.');
},);itRenders('with a warning for useState inside useMemo', async render => {function App() {useMemo(() => {useState();return 0;});return 'hi';}const domNode = await render(<App />, 1);expect(domNode.textContent).toEqual('hi');
});itRenders('with a warning for useRef inside useState', async render => {function App() {const [value] = useState(() => {
useRef(0);return 0;});return value;}const domNode = await render(<App />, 1);expect(domNode.textContent).toEqual('0');
});});describe('useRef', () => {itRenders('basic render', async render => {function Counter(props) {const ref = useRef();return <span ref={ref}>Hi</span>;}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('Hi');
});itRenders('multiple times when updates happen during the render phase',async render => {function Counter(props) {const [count, setCount] = useState(0);
const ref = useRef();if (count < 3) {const newCount = count + 1;
setCount(newCount);}yieldValue(count);return <span ref={ref}>Count: {count}</span>;}const domNode = await render(<Counter />);expect(clearLog()).toEqual([0, 1, 2, 3]);
expect(domNode.textContent).toEqual('Count: 3');
},);itRenders('always return the same reference through multiple renders',async render => {let firstRef = null;function Counter(props) {const [count, setCount] = useState(0);
const ref = useRef();if (firstRef === null) {firstRef = ref;} else if (firstRef !== ref) {throw new Error('should never change');}if (count < 3) {setCount(count + 1);
} else {firstRef = null;}yieldValue(count);return <span ref={ref}>Count: {count}</span>;}const domNode = await render(<Counter />);expect(clearLog()).toEqual([0, 1, 2, 3]);
expect(domNode.textContent).toEqual('Count: 3');
},);});describe('useEffect', () => {const yields = [];
itRenders('should ignore effects on the server', async render => {function Counter(props) {useEffect(() => {yieldValue('invoked on client');});return <Text text={'Count: ' + props.count} />;
}const domNode = await render(<Counter count={0} />);
yields.push(clearLog());
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});it('verifies yields in order', () => {expect(yields).toEqual([
['Count: 0'], // server render
['Count: 0'], // server stream
['Count: 0', 'invoked on client'], // clean render
['Count: 0', 'invoked on client'], // hydrated render
// nothing yielded for bad markup]);});});describe('useCallback', () => {itRenders('should not invoke the passed callbacks', async render => {function Counter(props) {useCallback(() => {yieldValue('should not be invoked');});return <Text text={'Count: ' + props.count} />;
}const domNode = await render(<Counter count={0} />);
expect(clearLog()).toEqual(['Count: 0']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});itRenders('should support render time callbacks', async render => {function Counter(props) {const renderCount = useCallback(increment => {return 'Count: ' + (props.count + increment);
});return <Text text={renderCount(3)} />;}const domNode = await render(<Counter count={2} />);
expect(clearLog()).toEqual(['Count: 5']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 5');
});itRenders('should only change the returned reference when the inputs change',async render => {function CapitalizedText(props) {const [text, setText] = useState(props.text);
const [count, setCount] = useState(0);
const capitalizeText = useCallback(() => text.toUpperCase(), [text]);
yieldValue(capitalizeText);if (count < 3) {setCount(count + 1);
}if (text === 'hello' && count === 2) {setText('hello, world.');
}return <Text text={capitalizeText()} />;}const domNode = await render(<CapitalizedText text="hello" />);const [first, second, third, fourth, result] = clearLog();
expect(first).toBe(second);
expect(second).toBe(third);
expect(third).not.toBe(fourth);
expect(result).toEqual('HELLO, WORLD.');
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('HELLO, WORLD.');
},);});describe('useImperativeHandle', () => {it('should not be invoked on the server', async () => {function Counter(props, ref) {useImperativeHandle(ref, () => {throw new Error('should not be invoked');});return <Text text={props.label + ': ' + ref.current} />;
}Counter = forwardRef(Counter);const counter = React.createRef();
counter.current = 0;
const domNode = await serverRender(<Counter label="Count" ref={counter} />,);expect(clearLog()).toEqual(['Count: 0']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});});describe('useInsertionEffect', () => {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(clearLog()).toEqual(['Count: 0']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});});describe('useLayoutEffect', () => {it('should warn when invoked during render', async () => {function Counter() {useLayoutEffect(() => {throw new Error('should not be invoked');});return <Text text="Count: 0" />;}const domNode = await serverRender(<Counter />, 1);expect(clearLog()).toEqual(['Count: 0']);
expect(domNode.tagName).toEqual('SPAN');
expect(domNode.textContent).toEqual('Count: 0');
});});describe('useContext', () => {itThrowsWhenRendering('if used inside a class component',async render => {const Context = React.createContext({}, () => {});
class Counter extends React.Component {
render() {const [count] = useContext(Context);
return <Text text={count} />;}}return render(<Counter />);},'Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for' +
' one of the following reasons:\n' +
'1. You might have mismatching versions of React and the renderer (such as React DOM)\n' +
'2. You might be breaking the Rules of Hooks\n' +
'3. You might have more than one copy of React in the same app\n' +
'See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.',
);});describe('invalid hooks', () => {it('warns when calling useRef inside useReducer', async () => {function App() {const [value, dispatch] = useReducer((state, action) => {
useRef(0);return state + 1;
}, 0);if (value === 0) {dispatch();}return value;}let error;try {await serverRender(<App />);} catch (x) {error = x;}expect(error).not.toBe(undefined);
expect(error.message).toContain(
'Rendered more hooks than during the previous render',);});});itRenders('can use the same context multiple times in the same function',async render => {const Context = React.createContext({foo: 0, bar: 0, baz: 0});
function Provider(props) {return (<Context.Provider
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
{props.children}
</Context.Provider>
);}function FooAndBar() {const {foo} = useContext(Context);const {bar} = useContext(Context);return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}function Baz() {const {baz} = useContext(Context);return <Text text={'Baz: ' + baz} />;
}class Indirection extends React.Component {
render() {return this.props.children;
}}function App(props) {return (<div><Provider foo={props.foo} bar={props.bar} baz={props.baz}>
<Indirection><Indirection><FooAndBar /></Indirection><Indirection><Baz /></Indirection></Indirection></Provider></div>);}const domNode = await render(<App foo={1} bar={3} baz={5} />);
expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
expect(domNode.childNodes.length).toBe(2);
expect(domNode.firstChild.tagName).toEqual('SPAN');
expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
expect(domNode.lastChild.tagName).toEqual('SPAN');
expect(domNode.lastChild.textContent).toEqual('Baz: 5');
},);describe('useDebugValue', () => {itRenders('is a noop', async render => {function Counter(props) {const debugValue = useDebugValue(123);return <Text text={typeof debugValue} />;}const domNode = await render(<Counter />);expect(domNode.textContent).toEqual('undefined');
});});describe('readContext', () => {function readContext(Context) {const dispatcher =React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.ReactCurrentDispatcher.current;
return dispatcher.readContext(Context);
}itRenders('can read the same context multiple times in the same function',async render => {const Context = React.createContext(
{foo: 0, bar: 0, baz: 0},(a, b) => {let result = 0;if (a.foo !== b.foo) {
result |= 0b001;
}if (a.bar !== b.bar) {
result |= 0b010;
}if (a.baz !== b.baz) {
result |= 0b100;
}return result;},);function Provider(props) {return (<Context.Provider
value={{foo: props.foo, bar: props.bar, baz: props.baz}}>
{props.children}
</Context.Provider>
);}function FooAndBar() {const {foo} = readContext(Context, 0b001);const {bar} = readContext(Context, 0b010);return <Text text={`Foo: ${foo}, Bar: ${bar}`} />;
}function Baz() {const {baz} = readContext(Context, 0b100);return <Text text={'Baz: ' + baz} />;
}class Indirection extends React.Component {
shouldComponentUpdate() {return false;}render() {return this.props.children;
}}function App(props) {return (<div><Provider foo={props.foo} bar={props.bar} baz={props.baz}>
<Indirection><Indirection><FooAndBar /></Indirection><Indirection><Baz /></Indirection></Indirection></Provider></div>);}const domNode = await render(<App foo={1} bar={3} baz={5} />);
expect(clearLog()).toEqual(['Foo: 1, Bar: 3', 'Baz: 5']);
expect(domNode.childNodes.length).toBe(2);
expect(domNode.firstChild.tagName).toEqual('SPAN');
expect(domNode.firstChild.textContent).toEqual('Foo: 1, Bar: 3');
expect(domNode.lastChild.tagName).toEqual('SPAN');
expect(domNode.lastChild.textContent).toEqual('Baz: 5');
},);itRenders('with a warning inside useMemo and useReducer', async render => {const Context = React.createContext(42);
function ReadInMemo(props) {const count = React.useMemo(() => readContext(Context), []);
return <Text text={count} />;}function ReadInReducer(props) {const [count, dispatch] = React.useReducer(() => readContext(Context));
if (count !== 42) {dispatch();}return <Text text={count} />;}const domNode1 = await render(<ReadInMemo />, 1);expect(domNode1.textContent).toEqual('42');
const domNode2 = await render(<ReadInReducer />, 1);expect(domNode2.textContent).toEqual('42');
});});it('renders successfully after a component using hooks throws an error', () => {function ThrowingComponent() {const [value, dispatch] = useReducer((state, action) => {
return state + 1;
}, 0);// throw an error if the count gets too high during the re-render phaseif (value >= 3) {throw new Error('Error from ThrowingComponent');} else {// dispatch to trigger a re-render of the componentdispatch();}return <div>{value}</div>;}function NonThrowingComponent() {const [count] = useState(0);
return <div>{count}</div>;}// First, render a component that will throw an error during a re-render triggered// by a dispatch call.
expect(() => ReactDOMServer.renderToString(<ThrowingComponent />)).toThrow(
'Error from ThrowingComponent',);// Next, assert that we can render a function component using hooks immediately// after an error occurred, which indictates the internal hooks state has been// reset.
const container = document.createElement('div');
container.innerHTML = ReactDOMServer.renderToString(
<NonThrowingComponent />,);expect(container.children[0].textContent).toEqual('0');
});});