'use strict';
let React;
let ReactNoop;
let Suspense;
let Scheduler;
let act;
let waitForAll;
let assertLog;
describe('memo', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
({Suspense} = React);
const InternalTestUtils = require('internal-test-utils');
waitForAll = InternalTestUtils.waitForAll;
assertLog = InternalTestUtils.assertLog;
});
function Text(props) {
Scheduler.log(props.text);
return <span prop={props.text} />;
}
async function fakeImport(result) {
return {default: result};
}
it('warns when giving a ref (simple)', async () => {
function App() {
return null;
}
App = React.memo(App);
function Outer() {
return <App ref={() => {}} />;
}
ReactNoop.render(<Outer />);
await expect(async () => await waitForAll([])).toErrorDev([
'Function components cannot be given refs. Attempts to access ' +
'this ref will fail.',
]);
});
it('warns when giving a ref (complex)', async () => {
function App() {
return null;
}
App = React.memo(App, () => false);
function Outer() {
return <App ref={() => {}} />;
}
ReactNoop.render(<Outer />);
await expect(async () => await waitForAll([])).toErrorDev([
'Function components cannot be given refs. Attempts to access ' +
'this ref will fail.',
]);
});
sharedTests('normal', (...args) => {
const Memo = React.memo(...args);
function Indirection(props) {
return <Memo {...props} />;
}
return React.lazy(() => fakeImport(Indirection));
});
sharedTests('lazy', (...args) => {
const Memo = React.memo(...args);
return React.lazy(() => fakeImport(Memo));
});
function sharedTests(label, memo) {
describe(`${label}`, () => {
it('bails out on props equality', async () => {
function Counter({count}) {
return <Text text={count} />;
}
Counter = memo(Counter);
await act(() =>
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Counter count={0} />
</Suspense>,
),
);
assertLog(['Loading...', 0]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
ReactNoop.render(
<Suspense>
<Counter count={0} />
</Suspense>,
);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
ReactNoop.render(
<Suspense>
<Counter count={1} />
</Suspense>,
);
await waitForAll([1]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
});
it("does not bail out if there's a context change", async () => {
const CountContext = React.createContext(0);
function readContext(Context) {
const dispatcher =
React
.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE
.H;
return dispatcher.readContext(Context);
}
function Counter(props) {
const count = readContext(CountContext);
return <Text text={`${props.label}: ${count}`} />;
}
Counter = memo(Counter);
class Parent extends React.Component {
state = {count: 0};
render() {
return (
<Suspense fallback={<Text text="Loading..." />}>
<CountContext.Provider value={this.state.count}>
<Counter label="Count" />
</CountContext.Provider>
</Suspense>
);
}
}
const parent = React.createRef(null);
await act(() => ReactNoop.render(<Parent ref={parent} />));
assertLog(['Loading...', 'Count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
ReactNoop.render(<Parent ref={parent} />);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 0" />);
parent.current.setState({count: 1});
await waitForAll(['Count: 1']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="Count: 1" />);
});
it('consistent behavior for reusing props object across different function component types', async () => {
const {useEffect, useState} = React;
let setSimpleMemoStep;
const SimpleMemo = React.memo(props => {
const [step, setStep] = useState(0);
setSimpleMemoStep = setStep;
const prevProps = React.useRef(props);
useEffect(() => {
if (props !== prevProps.current) {
prevProps.current = props;
Scheduler.log('Props changed [SimpleMemo]');
}
}, [props]);
return <Text text={`SimpleMemo [${props.prop}${step}]`} />;
});
let setComplexMemo;
const ComplexMemo = React.memo(
React.forwardRef((props, ref) => {
const [step, setStep] = useState(0);
setComplexMemo = setStep;
const prevProps = React.useRef(props);
useEffect(() => {
if (props !== prevProps.current) {
prevProps.current = props;
Scheduler.log('Props changed [ComplexMemo]');
}
}, [props]);
return <Text text={`ComplexMemo [${props.prop}${step}]`} />;
}),
);
let setMemoWithIndirectionStep;
const MemoWithIndirection = React.memo(props => {
return <Indirection props={props} />;
});
function Indirection({props}) {
const [step, setStep] = useState(0);
setMemoWithIndirectionStep = setStep;
const prevProps = React.useRef(props);
useEffect(() => {
if (props !== prevProps.current) {
prevProps.current = props;
Scheduler.log('Props changed [MemoWithIndirection]');
}
}, [props]);
return <Text text={`MemoWithIndirection [${props.prop}${step}]`} />;
}
function setLocalUpdateOnChildren(step) {
setSimpleMemoStep(step);
setMemoWithIndirectionStep(step);
setComplexMemo(step);
}
function App({prop}) {
return (
<>
<SimpleMemo prop={prop} />
<ComplexMemo prop={prop} />
<MemoWithIndirection prop={prop} />
</>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App prop="A" />);
});
assertLog([
'SimpleMemo [A0]',
'ComplexMemo [A0]',
'MemoWithIndirection [A0]',
]);
await act(() => {
root.render(<App prop="B" />);
});
assertLog([
'SimpleMemo [B0]',
'ComplexMemo [B0]',
'MemoWithIndirection [B0]',
'Props changed [SimpleMemo]',
'Props changed [ComplexMemo]',
'Props changed [MemoWithIndirection]',
]);
await act(() => {
root.render(<App prop="B" />);
});
assertLog([]);
await act(() => {
root.render(<App prop="B" />);
setLocalUpdateOnChildren(1);
});
assertLog([
'SimpleMemo [B1]',
'ComplexMemo [B1]',
'MemoWithIndirection [B1]',
]);
await act(() => {
root.render(<App prop="B" />);
setLocalUpdateOnChildren(2);
});
assertLog([
'SimpleMemo [B2]',
'ComplexMemo [B2]',
'MemoWithIndirection [B2]',
]);
});
it('accepts custom comparison function', async () => {
function Counter({count}) {
return <Text text={count} />;
}
Counter = memo(Counter, (oldProps, newProps) => {
Scheduler.log(
`Old count: ${oldProps.count}, New count: ${newProps.count}`,
);
return oldProps.count === newProps.count;
});
await act(() =>
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Counter count={0} />
</Suspense>,
),
);
assertLog(['Loading...', 0]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
ReactNoop.render(
<Suspense>
<Counter count={0} />
</Suspense>,
);
await waitForAll(['Old count: 0, New count: 0']);
expect(ReactNoop).toMatchRenderedOutput(<span prop={0} />);
ReactNoop.render(
<Suspense>
<Counter count={1} />
</Suspense>,
);
await waitForAll(['Old count: 0, New count: 1', 1]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={1} />);
});
it('supports non-pure class components', async () => {
class CounterInner extends React.Component {
static defaultProps = {suffix: '!'};
render() {
return <Text text={this.props.count + String(this.props.suffix)} />;
}
}
const Counter = memo(CounterInner);
await act(() =>
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Counter count={0} />
</Suspense>,
),
);
assertLog(['Loading...', '0!']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="0!" />);
ReactNoop.render(
<Suspense>
<Counter count={0} />
</Suspense>,
);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop="0!" />);
ReactNoop.render(
<Suspense>
<Counter count={1} />
</Suspense>,
);
await waitForAll(['1!']);
expect(ReactNoop).toMatchRenderedOutput(<span prop="1!" />);
});
it('supports defaultProps defined on the memo() return value', async () => {
function Counter({a, b, c, d, e}) {
return <Text text={a + b + c + d + e} />;
}
Counter.defaultProps = {
a: 1,
};
Counter = React.memo(Counter);
Counter.defaultProps = {
b: 2,
};
Counter = React.memo(Counter);
Counter = React.memo(Counter);
Counter.defaultProps = {
c: 3,
};
Counter = React.memo(Counter);
Counter.defaultProps = {
d: 4,
};
Counter = memo(Counter);
await expect(async () => {
await act(() => {
ReactNoop.render(
<Suspense fallback={<Text text="Loading..." />}>
<Counter e={5} />
</Suspense>,
);
});
assertLog(['Loading...', 15]);
}).toErrorDev([
'Counter: Support for defaultProps will be removed from memo components in a future major release. Use JavaScript default parameters instead.',
]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={15} />);
ReactNoop.render(
<Suspense>
<Counter e={5} />
</Suspense>,
);
await waitForAll([]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={15} />);
ReactNoop.render(
<Suspense>
<Counter e={10} />
</Suspense>,
);
await waitForAll([20]);
expect(ReactNoop).toMatchRenderedOutput(<span prop={20} />);
});
it('warns if the first argument is undefined', () => {
expect(() => memo()).toErrorDev(
'memo: The first argument must be a component. Instead ' +
'received: undefined',
{withoutStack: true},
);
});
it('warns if the first argument is null', () => {
expect(() => memo(null)).toErrorDev(
'memo: The first argument must be a component. Instead ' +
'received: null',
{withoutStack: true},
);
});
it('handles nested defaultProps declarations', async () => {
function Inner(props) {
return props.inner + props.middle + props.outer;
}
Inner.defaultProps = {inner: 1};
const Middle = React.memo(Inner);
Middle.defaultProps = {middle: 10};
const Outer = React.memo(Middle);
Outer.defaultProps = {outer: 100};
const root = ReactNoop.createRoot();
await expect(async () => {
await act(() => {
root.render(
<div>
<Outer />
</div>,
);
});
}).toErrorDev([
'Support for defaultProps will be removed from memo component',
]);
expect(root).toMatchRenderedOutput(<div>111</div>);
await act(async () => {
root.render(
<div>
<Outer inner="2" middle="3" outer="4" />
</div>,
);
await waitForAll([]);
});
expect(root).toMatchRenderedOutput(<div>234</div>);
});
it('does not drop lower priority state updates when bailing out at higher pri (simple)', async () => {
const {useState} = React;
let setCounter;
const Counter = memo(() => {
const [counter, _setCounter] = useState(0);
setCounter = _setCounter;
return counter;
});
function App() {
return (
<Suspense fallback="Loading...">
<Counter />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
expect(root).toMatchRenderedOutput('0');
await act(() => {
setCounter(1);
ReactNoop.discreteUpdates(() => {
root.render(<App />);
});
});
expect(root).toMatchRenderedOutput('1');
});
it('does not drop lower priority state updates when bailing out at higher pri (complex)', async () => {
const {useState} = React;
let setCounter;
const Counter = memo(
() => {
const [counter, _setCounter] = useState(0);
setCounter = _setCounter;
return counter;
},
(a, b) => a.complexProp.val === b.complexProp.val,
);
function App() {
return (
<Suspense fallback="Loading...">
<Counter complexProp={{val: 1}} />
</Suspense>
);
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<App />);
});
expect(root).toMatchRenderedOutput('0');
await act(() => {
setCounter(1);
ReactNoop.discreteUpdates(() => {
root.render(<App />);
});
});
expect(root).toMatchRenderedOutput('1');
});
});
it('should skip memo in the stack if neither displayName nor name are present', async () => {
const MemoComponent = React.memo(props => [<span />]);
ReactNoop.render(
<p>
<MemoComponent />
</p>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'Each child in a list should have a unique "key" prop. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in ',
);
});
it('should use the inner function name for the stack', async () => {
const MemoComponent = React.memo(function Inner(props, ref) {
return [<span />];
});
ReactNoop.render(
<p>
<MemoComponent />
</p>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Inner (at **)' +
(gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
);
});
it('should use the inner name in the stack', async () => {
const fn = (props, ref) => {
return [<span />];
};
Object.defineProperty(fn, 'name', {value: 'Inner'});
const MemoComponent = React.memo(fn);
ReactNoop.render(
<p>
<MemoComponent />
</p>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Inner (at **)' +
(gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
);
});
it('can use the outer displayName in the stack', async () => {
const MemoComponent = React.memo((props, ref) => {
return [<span />];
});
MemoComponent.displayName = 'Outer';
ReactNoop.render(
<p>
<MemoComponent />
</p>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <Outer>. It was passed a child from Outer. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Outer (at **)' +
(gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
);
});
it('should prefer the inner to the outer displayName in the stack', async () => {
const fn = (props, ref) => {
return [<span />];
};
Object.defineProperty(fn, 'name', {value: 'Inner'});
const MemoComponent = React.memo(fn);
MemoComponent.displayName = 'Outer';
ReactNoop.render(
<p>
<MemoComponent />
</p>,
);
await expect(async () => {
await waitForAll([]);
}).toErrorDev(
'Each child in a list should have a unique "key" prop.' +
'\n\nCheck the top-level render call using <Inner>. It was passed a child from Inner. ' +
'See https://react.dev/link/warning-keys for more information.\n' +
' in span (at **)\n' +
' in Inner (at **)' +
(gate(flags => flags.enableOwnerStacks) ? '' : '\n in p (at **)'),
);
});
}
});