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
});