diff --git a/packages/react-reconciler/src/__tests__/useMemoCache-test.js b/packages/react-reconciler/src/__tests__/useMemoCache-test.js index 7dd58514be..94571df0f9 100644 --- a/packages/react-reconciler/src/__tests__/useMemoCache-test.js +++ b/packages/react-reconciler/src/__tests__/useMemoCache-test.js @@ -74,31 +74,37 @@ describe('useMemoCache()', () => { let setX; let forceUpdate; function Component(props) { - const cache = useMemoCache(4); + const cache = useMemoCache(5); // x is used to produce a `data` object passed to the child const [x, _setX] = useState(0); setX = _setX; - const c_x = x !== cache[0]; - cache[0] = x; // n is passed as-is to the child as a cache breaker const [n, setN] = useState(0); forceUpdate = () => setN(a => a + 1); - const c_n = n !== cache[1]; - cache[1] = n; + const c_0 = x !== cache[0]; let data; - if (c_x) { - data = cache[2] = {text: `Count ${x}`}; + if (c_0) { + data = {text: `Count ${x}`}; + cache[0] = x; + cache[1] = data; } else { - data = cache[2]; + data = cache[1]; } - if (c_x || c_n) { - return (cache[3] = ); + const c_2 = x !== cache[2]; + const c_3 = n !== cache[3]; + let t0; + if (c_2 || c_3) { + t0 = ; + cache[2] = x; + cache[3] = n; + cache[4] = t0; } else { - return cache[3]; + t0 = cache[4]; } + return t0; } let data; const Text = jest.fn(function Text(props) { @@ -135,132 +141,117 @@ describe('useMemoCache()', () => { // @gate enableUseMemoCacheHook test('update component using cache with setstate during render', async () => { - let setX; let setN; function Component(props) { - const cache = useMemoCache(4); + const cache = useMemoCache(5); // x is used to produce a `data` object passed to the child - const [x, _setX] = useState(0); - setX = _setX; - const c_x = x !== cache[0]; - cache[0] = x; + 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]; + } // n is passed as-is to the child as a cache breaker const [n, _setN] = useState(0); setN = _setN; - const c_n = n !== cache[1]; - cache[1] = n; - // NOTE: setstate and early return here means that x will update - // without the data value being updated. Subsequent renders could - // therefore think that c_x = false (hasn't changed) and skip updating - // data. - // The memoizing compiler will have to handle this case, but the runtime - // can help by falling back to resetting the cache if a setstate occurs - // during render (this mirrors what we do for useMemo and friends) if (n === 1) { setN(2); return; } - let data; - if (c_x) { - data = cache[2] = {text: `Count ${x}`}; + const c_2 = x !== cache[2]; + const c_3 = n !== cache[3]; + let t0; + if (c_2 || c_3) { + t0 = ; + cache[2] = x; + cache[3] = n; + cache[4] = t0; } else { - data = cache[2]; - } - if (c_x || c_n) { - return (cache[3] = ); - } else { - return cache[3]; + t0 = cache[4]; } + return t0; } let data; const Text = jest.fn(function Text(props) { data = props.data; - return data.text; + return `${data.text} (n=${props.n})`; }); const root = ReactNoop.createRoot(); await act(() => { root.render(); }); - expect(root).toMatchRenderedOutput('Count 0'); + expect(root).toMatchRenderedOutput('Count 0 (n=0)'); expect(Text).toBeCalledTimes(1); const data0 = data; - // Simultaneously trigger an update to x (should create a new data value) - // and trigger the setState+early return. The runtime should reset the cache - // to avoid an inconsistency + // Trigger an update that will cause a setState during render. The `data` prop + // does not depend on `n`, and should remain cached. await act(() => { - setX(1); setN(1); }); - expect(root).toMatchRenderedOutput('Count 1'); + expect(root).toMatchRenderedOutput('Count 0 (n=2)'); expect(Text).toBeCalledTimes(2); - expect(data).not.toBe(data0); - const data1 = data; - - // Forcing an unrelated update shouldn't recreate the - // data object. - await act(() => { - setN(3); - }); - expect(root).toMatchRenderedOutput('Count 1'); - expect(Text).toBeCalledTimes(3); - expect(data).toBe(data1); // confirm that the cache persisted across renders + expect(data).toBe(data0); }); // @gate enableUseMemoCacheHook test('update component using cache with throw during render', async () => { - let setX; let setN; let shouldFail = true; function Component(props) { - const cache = useMemoCache(4); + const cache = useMemoCache(5); // x is used to produce a `data` object passed to the child - const [x, _setX] = useState(0); - setX = _setX; - const c_x = x !== cache[0]; - cache[0] = x; + 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]; + } // n is passed as-is to the child as a cache breaker const [n, _setN] = useState(0); setN = _setN; - const c_n = n !== cache[1]; - cache[1] = n; - // NOTE the initial failure will trigger a re-render, after which the function - // will early return. This validates that the runtime resets the cache on error: - // if it doesn't the cache will be corrupt, with the cached version of data - // out of data from the cached version of x. if (n === 1) { if (shouldFail) { shouldFail = false; throw new Error('failed'); } - setN(2); - return; } - let data; - if (c_x) { - data = cache[2] = {text: `Count ${x}`}; + const c_2 = x !== cache[2]; + const c_3 = n !== cache[3]; + let t0; + if (c_2 || c_3) { + t0 = ; + cache[2] = x; + cache[3] = n; + cache[4] = t0; } else { - data = cache[2]; - } - if (c_x || c_n) { - return (cache[3] = ); - } else { - return cache[3]; + t0 = cache[4]; } + return t0; } let data; const Text = jest.fn(function Text(props) { data = props.data; - return data.text; + return `${data.text} (n=${props.n})`; }); spyOnDev(console, 'error'); @@ -273,30 +264,25 @@ describe('useMemoCache()', () => { , ); }); - expect(root).toMatchRenderedOutput('Count 0'); + expect(root).toMatchRenderedOutput('Count 0 (n=0)'); expect(Text).toBeCalledTimes(1); const data0 = data; - // Simultaneously trigger an update to x (should create a new data value) - // and trigger the setState+early return. The runtime should reset the cache - // to avoid an inconsistency await act(() => { - // this update bumps the count - setX(1); // this triggers a throw. setN(1); }); - expect(root).toMatchRenderedOutput('Count 1'); + expect(root).toMatchRenderedOutput('Count 0 (n=1)'); expect(Text).toBeCalledTimes(2); - expect(data).not.toBe(data0); + expect(data).toBe(data0); const data1 = data; // Forcing an unrelated update shouldn't recreate the // data object. await act(() => { - setN(3); + setN(2); }); - expect(root).toMatchRenderedOutput('Count 1'); + expect(root).toMatchRenderedOutput('Count 0 (n=2)'); expect(Text).toBeCalledTimes(3); expect(data).toBe(data1); // confirm that the cache persisted across renders });