@samitouri / QOS-React-2 / commits / 0ffc7f632b

Update useMemoCache test to confirm that cache persists across errors (#26510)

## Summary Updates the `useMemoCache()` tests to validate that the memo cache persists when a component does a setState during render or throws during render. Forget's compilation output follows the general pattern used in this test and is resilient to rendering running partway and then again with different inputs. ## How did you test this change? `yarn test` (this is a test-only change)

Joseph Savona committed Mar 30, 2023 at 07:47 UTC 0ffc7f632b5cdc2d4ede97b6f8ff55e02183bdf9
1 file changed +73 -87
packages/react-reconciler/src/__tests__/useMemoCache-test.js
+73 -87
@@ -74,31 +74,37 @@ describe('useMemoCache()', () => {
74 let setX;
75 let forceUpdate;
76 function Component(props) {
77 - const cache = useMemoCache(4);
77 + const cache = useMemoCache(5);
78
79 // x is used to produce a `data` object passed to the child
80 const [x, _setX] = useState(0);
81 setX = _setX;
82 - const c_x = x !== cache[0];
83 - cache[0] = x;
82
83 // n is passed as-is to the child as a cache breaker
84 const [n, setN] = useState(0);
85 forceUpdate = () => setN(a => a + 1);
88 - const c_n = n !== cache[1];
89 - cache[1] = n;
86
87 + const c_0 = x !== cache[0];
88 let data;
92 - if (c_x) {
93 - data = cache[2] = {text: `Count ${x}`};
89 + if (c_0) {
90 + data = {text: `Count ${x}`};
91 + cache[0] = x;
92 + cache[1] = data;
93 } else {
95 - data = cache[2];
94 + data = cache[1];
95 }
97 - if (c_x || c_n) {
98 - return (cache[3] = <Text data={data} n={n} />);
96 + const c_2 = x !== cache[2];
97 + const c_3 = n !== cache[3];
98 + let t0;
99 + if (c_2 || c_3) {
100 + t0 = <Text data={data} n={n} />;
101 + cache[2] = x;
102 + cache[3] = n;
103 + cache[4] = t0;
104 } else {
100 - return cache[3];
105 + t0 = cache[4];
106 }
107 + return t0;
108 }
109 let data;
110 const Text = jest.fn(function Text(props) {
@@ -135,132 +141,117 @@ describe('useMemoCache()', () => {
141
142 // @gate enableUseMemoCacheHook
143 test('update component using cache with setstate during render', async () => {
138 - let setX;
144 let setN;
145 function Component(props) {
141 - const cache = useMemoCache(4);
146 + const cache = useMemoCache(5);
147
148 // x is used to produce a `data` object passed to the child
144 - const [x, _setX] = useState(0);
145 - setX = _setX;
146 - const c_x = x !== cache[0];
147 - cache[0] = x;
149 + const [x] = useState(0);
150 +
151 + const c_0 = x !== cache[0];
152 + let data;
153 + if (c_0) {
154 + data = {text: `Count ${x}`};
155 + cache[0] = x;
156 + cache[1] = data;
157 + } else {
158 + data = cache[1];
159 + }
160
161 // n is passed as-is to the child as a cache breaker
162 const [n, _setN] = useState(0);
163 setN = _setN;
152 - const c_n = n !== cache[1];
153 - cache[1] = n;
164
155 - // NOTE: setstate and early return here means that x will update
156 - // without the data value being updated. Subsequent renders could
157 - // therefore think that c_x = false (hasn't changed) and skip updating
158 - // data.
159 - // The memoizing compiler will have to handle this case, but the runtime
160 - // can help by falling back to resetting the cache if a setstate occurs
161 - // during render (this mirrors what we do for useMemo and friends)
165 if (n === 1) {
166 setN(2);
167 return;
168 }
169
167 - let data;
168 - if (c_x) {
169 - data = cache[2] = {text: `Count ${x}`};
170 + const c_2 = x !== cache[2];
171 + const c_3 = n !== cache[3];
172 + let t0;
173 + if (c_2 || c_3) {
174 + t0 = <Text data={data} n={n} />;
175 + cache[2] = x;
176 + cache[3] = n;
177 + cache[4] = t0;
178 } else {
171 - data = cache[2];
172 - }
173 - if (c_x || c_n) {
174 - return (cache[3] = <Text data={data} n={n} />);
175 - } else {
176 - return cache[3];
179 + t0 = cache[4];
180 }
181 + return t0;
182 }
183 let data;
184 const Text = jest.fn(function Text(props) {
185 data = props.data;
182 - return data.text;
186 + return `${data.text} (n=${props.n})`;
187 });
188
189 const root = ReactNoop.createRoot();
190 await act(() => {
191 root.render(<Component />);
192 });
189 - expect(root).toMatchRenderedOutput('Count 0');
193 + expect(root).toMatchRenderedOutput('Count 0 (n=0)');
194 expect(Text).toBeCalledTimes(1);
195 const data0 = data;
196
193 - // Simultaneously trigger an update to x (should create a new data value)
194 - // and trigger the setState+early return. The runtime should reset the cache
195 - // to avoid an inconsistency
197 + // Trigger an update that will cause a setState during render. The `data` prop
198 + // does not depend on `n`, and should remain cached.
199 await act(() => {
197 - setX(1);
200 setN(1);
201 });
200 - expect(root).toMatchRenderedOutput('Count 1');
202 + expect(root).toMatchRenderedOutput('Count 0 (n=2)');
203 expect(Text).toBeCalledTimes(2);
202 - expect(data).not.toBe(data0);
203 - const data1 = data;
204 -
205 - // Forcing an unrelated update shouldn't recreate the
206 - // data object.
207 - await act(() => {
208 - setN(3);
209 - });
210 - expect(root).toMatchRenderedOutput('Count 1');
211 - expect(Text).toBeCalledTimes(3);
212 - expect(data).toBe(data1); // confirm that the cache persisted across renders
204 + expect(data).toBe(data0);
205 });
206
207 // @gate enableUseMemoCacheHook
208 test('update component using cache with throw during render', async () => {
217 - let setX;
209 let setN;
210 let shouldFail = true;
211 function Component(props) {
221 - const cache = useMemoCache(4);
212 + const cache = useMemoCache(5);
213
214 // x is used to produce a `data` object passed to the child
224 - const [x, _setX] = useState(0);
225 - setX = _setX;
226 - const c_x = x !== cache[0];
227 - cache[0] = x;
215 + const [x] = useState(0);
216 +
217 + const c_0 = x !== cache[0];
218 + let data;
219 + if (c_0) {
220 + data = {text: `Count ${x}`};
221 + cache[0] = x;
222 + cache[1] = data;
223 + } else {
224 + data = cache[1];
225 + }
226
227 // n is passed as-is to the child as a cache breaker
228 const [n, _setN] = useState(0);
229 setN = _setN;
232 - const c_n = n !== cache[1];
233 - cache[1] = n;
230
235 - // NOTE the initial failure will trigger a re-render, after which the function
236 - // will early return. This validates that the runtime resets the cache on error:
237 - // if it doesn't the cache will be corrupt, with the cached version of data
238 - // out of data from the cached version of x.
231 if (n === 1) {
232 if (shouldFail) {
233 shouldFail = false;
234 throw new Error('failed');
235 }
244 - setN(2);
245 - return;
236 }
237
248 - let data;
249 - if (c_x) {
250 - data = cache[2] = {text: `Count ${x}`};
238 + const c_2 = x !== cache[2];
239 + const c_3 = n !== cache[3];
240 + let t0;
241 + if (c_2 || c_3) {
242 + t0 = <Text data={data} n={n} />;
243 + cache[2] = x;
244 + cache[3] = n;
245 + cache[4] = t0;
246 } else {
252 - data = cache[2];
253 - }
254 - if (c_x || c_n) {
255 - return (cache[3] = <Text data={data} n={n} />);
256 - } else {
257 - return cache[3];
247 + t0 = cache[4];
248 }
249 + return t0;
250 }
251 let data;
252 const Text = jest.fn(function Text(props) {
253 data = props.data;
263 - return data.text;
254 + return `${data.text} (n=${props.n})`;
255 });
256
257 spyOnDev(console, 'error');
@@ -273,30 +264,25 @@ describe('useMemoCache()', () => {
264 </ErrorBoundary>,
265 );
266 });
276 - expect(root).toMatchRenderedOutput('Count 0');
267 + expect(root).toMatchRenderedOutput('Count 0 (n=0)');
268 expect(Text).toBeCalledTimes(1);
269 const data0 = data;
270
280 - // Simultaneously trigger an update to x (should create a new data value)
281 - // and trigger the setState+early return. The runtime should reset the cache
282 - // to avoid an inconsistency
271 await act(() => {
284 - // this update bumps the count
285 - setX(1);
272 // this triggers a throw.
273 setN(1);
274 });
289 - expect(root).toMatchRenderedOutput('Count 1');
275 + expect(root).toMatchRenderedOutput('Count 0 (n=1)');
276 expect(Text).toBeCalledTimes(2);
291 - expect(data).not.toBe(data0);
277 + expect(data).toBe(data0);
278 const data1 = data;
279
280 // Forcing an unrelated update shouldn't recreate the
281 // data object.
282 await act(() => {
297 - setN(3);
283 + setN(2);
284 });
299 - expect(root).toMatchRenderedOutput('Count 1');
285 + expect(root).toMatchRenderedOutput('Count 0 (n=2)');
286 expect(Text).toBeCalledTimes(3);
287 expect(data).toBe(data1); // confirm that the cache persisted across renders
288 });