let React;
let ReactNoop;
let Scheduler;
let act;
let assertLog;
let useMemo;
let useState;
let useMemoCache;
let MemoCacheSentinel;
let ErrorBoundary;
describe('useMemoCache()', () => {
beforeEach(() => {
jest.resetModules();
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
act = require('internal-test-utils').act;
assertLog = require('internal-test-utils').assertLog;
useMemo = React.useMemo;
useMemoCache = require('react/compiler-runtime').c;
useState = React.useState;
MemoCacheSentinel = Symbol.for('react.memo_cache_sentinel');
class _ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = {hasError: false};
}
static getDerivedStateFromError(error) {
return {hasError: true};
}
componentDidCatch(error, errorInfo) {}
render() {
if (this.state.hasError) {
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
ErrorBoundary = _ErrorBoundary;
});
it('render component using cache', async () => {
function Component(props) {
const cache = useMemoCache(1);
expect(Array.isArray(cache)).toBe(true);
expect(cache.length).toBe(1);
expect(cache[0]).toBe(MemoCacheSentinel);
return 'Ok';
}
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Component />);
});
expect(root).toMatchRenderedOutput('Ok');
});
it('update component using cache', async () => {
let setX;
let forceUpdate;
function Component(props) {
const cache = useMemoCache(5);
const [x, _setX] = useState(0);
setX = _setX;
const [n, setN] = useState(0);
forceUpdate = () => setN(a => a + 1);
const c_0 = x !== cache[0];
let data;
if (c_0) {
data = {text: `Count ${x}`};
cache[0] = x;
cache[1] = data;
} else {
data = cache[1];
}
const c_2 = x !== cache[2];
const c_3 = n !== cache[3];
let t0;
if (c_2 || c_3) {
t0 = <Text data={data} n={n} />;
cache[2] = x;
cache[3] = n;
cache[4] = t0;
} else {
t0 = cache[4];
}
return t0;
}
let data;
const Text = jest.fn(function Text(props) {
data = props.data;
return data.text;
});
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Component />);
});
expect(root).toMatchRenderedOutput('Count 0');
expect(Text).toBeCalledTimes(1);
const data0 = data;
await act(() => {
setX(1);
});
expect(root).toMatchRenderedOutput('Count 1');
expect(Text).toBeCalledTimes(2);
expect(data).not.toBe(data0);
const data1 = data;
await act(() => {
forceUpdate();
});
expect(root).toMatchRenderedOutput('Count 1');
expect(Text).toBeCalledTimes(3);
expect(data).toBe(data1);
});
it('update component using cache with setstate during render', async () => {
let setN;
function Component(props) {
const cache = useMemoCache(5);
const [x] = useState(0);
const c_0 = x !== cache[0];
let data;
if (c_0) {
data = {text: `Count ${x}`};
cache[0] = x;
cache[1] = data;
} else {
data = cache[1];
}
const [n, _setN] = useState(0);
setN = _setN;
if (n === 1) {
setN(2);
return;
}
const c_2 = x !== cache[2];
const c_3 = n !== cache[3];
let t0;
if (c_2 || c_3) {
t0 = <Text data={data} n={n} />;
cache[2] = x;
cache[3] = n;
cache[4] = t0;
} else {
t0 = cache[4];
}
return t0;
}
let data;
const Text = jest.fn(function Text(props) {
data = props.data;
return `${data.text} (n=${props.n})`;
});
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Component />);
});
expect(root).toMatchRenderedOutput('Count 0 (n=0)');
expect(Text).toBeCalledTimes(1);
const data0 = data;
await act(() => {
setN(1);
});
expect(root).toMatchRenderedOutput('Count 0 (n=2)');
expect(Text).toBeCalledTimes(2);
expect(data).toBe(data0);
});
it('update component using cache with throw during render', async () => {
let setN;
let shouldFail = true;
function Component(props) {
const cache = useMemoCache(5);
const [x] = useState(0);
const c_0 = x !== cache[0];
let data;
if (c_0) {
data = {text: `Count ${x}`};
cache[0] = x;
cache[1] = data;
} else {
data = cache[1];
}
const [n, _setN] = useState(0);
setN = _setN;
if (n === 1) {
if (shouldFail) {
shouldFail = false;
throw new Error('failed');
}
}
const c_2 = x !== cache[2];
const c_3 = n !== cache[3];
let t0;
if (c_2 || c_3) {
t0 = <Text data={data} n={n} />;
cache[2] = x;
cache[3] = n;
cache[4] = t0;
} else {
t0 = cache[4];
}
return t0;
}
let data;
const Text = jest.fn(function Text(props) {
data = props.data;
return `${data.text} (n=${props.n})`;
});
spyOnDev(console, 'error');
const root = ReactNoop.createRoot();
await act(() => {
root.render(
<ErrorBoundary>
<Component />
</ErrorBoundary>,
);
});
expect(root).toMatchRenderedOutput('Count 0 (n=0)');
expect(Text).toBeCalledTimes(1);
const data0 = data;
await act(() => {
setN(1);
});
expect(root).toMatchRenderedOutput('Count 0 (n=1)');
expect(Text).toBeCalledTimes(2);
expect(data).toBe(data0);
const data1 = data;
await act(() => {
setN(2);
});
expect(root).toMatchRenderedOutput('Count 0 (n=2)');
expect(Text).toBeCalledTimes(3);
expect(data).toBe(data1);
});
it('update component and custom hook with caches', async () => {
let setX;
let forceUpdate;
function Component(props) {
const cache = useMemoCache(4);
const [x, _setX] = useState(0);
setX = _setX;
const c_x = x !== cache[0];
cache[0] = x;
const [n, setN] = useState(0);
forceUpdate = () => setN(a => a + 1);
const c_n = n !== cache[1];
cache[1] = n;
let _data;
if (c_x) {
_data = cache[2] = {text: `Count ${x}`};
} else {
_data = cache[2];
}
const data = useData(_data);
if (c_x || c_n) {
return (cache[3] = <Text data={data} n={n} />);
} else {
return cache[3];
}
}
function useData(data) {
const cache = useMemoCache(2);
const c_data = data !== cache[0];
cache[0] = data;
let nextData;
if (c_data) {
nextData = cache[1] = {text: data.text.toLowerCase()};
} else {
nextData = cache[1];
}
return nextData;
}
let data;
const Text = jest.fn(function Text(props) {
data = props.data;
return data.text;
});
const root = ReactNoop.createRoot();
await act(() => {
root.render(<Component />);
});
expect(root).toMatchRenderedOutput('count 0');
expect(Text).toBeCalledTimes(1);
const data0 = data;
await act(() => {
setX(1);
});
expect(root).toMatchRenderedOutput('count 1');
expect(Text).toBeCalledTimes(2);
expect(data).not.toBe(data0);
const data1 = data;
await act(() => {
forceUpdate();
});
expect(root).toMatchRenderedOutput('count 1');
expect(Text).toBeCalledTimes(3);
expect(data).toBe(data1);
});
it('reuses computations from suspended/interrupted render attempts during an update', async () => {
function someExpensiveProcessing(t) {
Scheduler.log(`Some expensive processing... [${t}]`);
return t;
}
function useWithLog(t, msg) {
try {
return React.use(t);
} catch (x) {
Scheduler.log(`Suspend! [${msg}]`);
throw x;
}
}
function Data(t0) {
const $ = useMemoCache(5);
const {chunkA, chunkB} = t0;
const t1 = useWithLog(chunkA, 'chunkA');
let t2;
if ($[0] !== t1) {
t2 = someExpensiveProcessing(t1);
$[0] = t1;
$[1] = t2;
} else {
t2 = $[1];
}
const a = t2;
const b = useWithLog(chunkB, 'chunkB');
let t3;
if ($[2] !== a || $[3] !== b) {
t3 = (
<>
{a}
{b}
</>
);
$[2] = a;
$[3] = b;
$[4] = t3;
} else {
t3 = $[4];
}
return t3;
}
let setInput;
function Input() {
const [input, _set] = useState('');
setInput = _set;
return input;
}
function App(t0) {
const $ = useMemoCache(4);
const {chunkA, chunkB} = t0;
let t1;
if ($[0] === Symbol.for('react.memo_cache_sentinel')) {
t1 = (
<div>
Input: <Input />
</div>
);
$[0] = t1;
} else {
t1 = $[0];
}
let t2;
if ($[1] !== chunkA || $[2] !== chunkB) {
t2 = (
<>
{t1}
<div>
Data: <Data chunkA={chunkA} chunkB={chunkB} />
</div>
</>
);
$[1] = chunkA;
$[2] = chunkB;
$[3] = t2;
} else {
t2 = $[3];
}
return t2;
}
function createInstrumentedResolvedPromise(value) {
return {
then() {},
status: 'fulfilled',
value,
};
}
function createDeferred() {
let resolve;
const p = new Promise(res => {
resolve = res;
});
p.resolve = resolve;
return p;
}
const root = ReactNoop.createRoot();
const initialChunkA = createInstrumentedResolvedPromise('A1');
const initialChunkB = createInstrumentedResolvedPromise('B1');
await act(() =>
root.render(<App chunkA={initialChunkA} chunkB={initialChunkB} />),
);
assertLog(['Some expensive processing... [A1]']);
expect(root).toMatchRenderedOutput(
<>
<div>Input: </div>
<div>Data: A1B1</div>
</>,
);
const updatedChunkA = createDeferred();
const updatedChunkB = createDeferred();
await act(() => {
React.startTransition(() => {
root.render(<App chunkA={updatedChunkA} chunkB={updatedChunkB} />);
});
});
assertLog(['Suspend! [chunkA]']);
await act(() => updatedChunkA.resolve('A2'));
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
expect(root).toMatchRenderedOutput(
<>
<div>Input: </div>
<div>Data: A1B1</div>
</>,
);
await act(() => setInput('hi!'));
if (gate(flags => flags.enableNoCloningMemoCache)) {
assertLog(['Suspend! [chunkB]']);
} else {
assertLog(['Some expensive processing... [A2]', 'Suspend! [chunkB]']);
}
expect(root).toMatchRenderedOutput(
<>
<div>Input: hi!</div>
<div>Data: A1B1</div>
</>,
);
await act(() => updatedChunkB.resolve('B2'));
if (gate(flags => flags.enableNoCloningMemoCache)) {
assertLog([]);
} else {
assertLog(['Some expensive processing... [A2]']);
}
expect(root).toMatchRenderedOutput(
<>
<div>Input: hi!</div>
<div>Data: A2B2</div>
</>,
);
});
it('(repro) infinite renders when used with setState during render', async () => {
function useCompilerMemo(value) {
let arr;
const $ = useMemoCache(2);
if ($[0] !== value) {
arr = [value];
$[0] = value;
$[1] = arr;
} else {
arr = $[1];
}
return arr;
}
function useManualMemo(value) {
return useMemo(() => [value], [value]);
}
function makeComponent(hook) {
return function Component({value}) {
const state = hook(value);
const [prevState, setPrevState] = useState(null);
if (state !== prevState) {
setPrevState(state);
}
return <div>{state.join(',')}</div>;
};
}
let root = ReactNoop.createRoot();
const CompilerMemoComponent = makeComponent(useCompilerMemo);
await act(() => {
root.render(<CompilerMemoComponent value={2} />);
});
expect(root).toMatchRenderedOutput(<div>2</div>);
root = ReactNoop.createRoot();
const HookMemoComponent = makeComponent(useManualMemo);
await act(() => {
root.render(<HookMemoComponent value={2} />);
});
expect(root).toMatchRenderedOutput(<div>2</div>);
});
});