Skip to content

Commit 0ffc7f6

Browse files
authored
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)
1 parent 29a3be7 commit 0ffc7f6

File tree

1 file changed

+73
-87
lines changed

1 file changed

+73
-87
lines changed

packages/react-reconciler/src/__tests__/useMemoCache-test.js

Lines changed: 73 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -74,31 +74,37 @@ describe('useMemoCache()', () => {
7474
let setX;
7575
let forceUpdate;
7676
function Component(props) {
77-
const cache = useMemoCache(4);
77+
const cache = useMemoCache(5);
7878

7979
// x is used to produce a `data` object passed to the child
8080
const [x, _setX] = useState(0);
8181
setX = _setX;
82-
const c_x = x !== cache[0];
83-
cache[0] = x;
8482

8583
// n is passed as-is to the child as a cache breaker
8684
const [n, setN] = useState(0);
8785
forceUpdate = () => setN(a => a + 1);
88-
const c_n = n !== cache[1];
89-
cache[1] = n;
9086

87+
const c_0 = x !== cache[0];
9188
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;
9493
} else {
95-
data = cache[2];
94+
data = cache[1];
9695
}
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;
99104
} else {
100-
return cache[3];
105+
t0 = cache[4];
101106
}
107+
return t0;
102108
}
103109
let data;
104110
const Text = jest.fn(function Text(props) {
@@ -135,132 +141,117 @@ describe('useMemoCache()', () => {
135141

136142
// @gate enableUseMemoCacheHook
137143
test('update component using cache with setstate during render', async () => {
138-
let setX;
139144
let setN;
140145
function Component(props) {
141-
const cache = useMemoCache(4);
146+
const cache = useMemoCache(5);
142147

143148
// 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+
}
148160

149161
// n is passed as-is to the child as a cache breaker
150162
const [n, _setN] = useState(0);
151163
setN = _setN;
152-
const c_n = n !== cache[1];
153-
cache[1] = n;
154164

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)
162165
if (n === 1) {
163166
setN(2);
164167
return;
165168
}
166169

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;
170178
} 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];
177180
}
181+
return t0;
178182
}
179183
let data;
180184
const Text = jest.fn(function Text(props) {
181185
data = props.data;
182-
return data.text;
186+
return `${data.text} (n=${props.n})`;
183187
});
184188

185189
const root = ReactNoop.createRoot();
186190
await act(() => {
187191
root.render(<Component />);
188192
});
189-
expect(root).toMatchRenderedOutput('Count 0');
193+
expect(root).toMatchRenderedOutput('Count 0 (n=0)');
190194
expect(Text).toBeCalledTimes(1);
191195
const data0 = data;
192196

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.
196199
await act(() => {
197-
setX(1);
198200
setN(1);
199201
});
200-
expect(root).toMatchRenderedOutput('Count 1');
202+
expect(root).toMatchRenderedOutput('Count 0 (n=2)');
201203
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);
213205
});
214206

215207
// @gate enableUseMemoCacheHook
216208
test('update component using cache with throw during render', async () => {
217-
let setX;
218209
let setN;
219210
let shouldFail = true;
220211
function Component(props) {
221-
const cache = useMemoCache(4);
212+
const cache = useMemoCache(5);
222213

223214
// 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+
}
228226

229227
// n is passed as-is to the child as a cache breaker
230228
const [n, _setN] = useState(0);
231229
setN = _setN;
232-
const c_n = n !== cache[1];
233-
cache[1] = n;
234230

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.
239231
if (n === 1) {
240232
if (shouldFail) {
241233
shouldFail = false;
242234
throw new Error('failed');
243235
}
244-
setN(2);
245-
return;
246236
}
247237

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;
251246
} 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];
258248
}
249+
return t0;
259250
}
260251
let data;
261252
const Text = jest.fn(function Text(props) {
262253
data = props.data;
263-
return data.text;
254+
return `${data.text} (n=${props.n})`;
264255
});
265256

266257
spyOnDev(console, 'error');
@@ -273,30 +264,25 @@ describe('useMemoCache()', () => {
273264
</ErrorBoundary>,
274265
);
275266
});
276-
expect(root).toMatchRenderedOutput('Count 0');
267+
expect(root).toMatchRenderedOutput('Count 0 (n=0)');
277268
expect(Text).toBeCalledTimes(1);
278269
const data0 = data;
279270

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
283271
await act(() => {
284-
// this update bumps the count
285-
setX(1);
286272
// this triggers a throw.
287273
setN(1);
288274
});
289-
expect(root).toMatchRenderedOutput('Count 1');
275+
expect(root).toMatchRenderedOutput('Count 0 (n=1)');
290276
expect(Text).toBeCalledTimes(2);
291-
expect(data).not.toBe(data0);
277+
expect(data).toBe(data0);
292278
const data1 = data;
293279

294280
// Forcing an unrelated update shouldn't recreate the
295281
// data object.
296282
await act(() => {
297-
setN(3);
283+
setN(2);
298284
});
299-
expect(root).toMatchRenderedOutput('Count 1');
285+
expect(root).toMatchRenderedOutput('Count 0 (n=2)');
300286
expect(Text).toBeCalledTimes(3);
301287
expect(data).toBe(data1); // confirm that the cache persisted across renders
302288
});

0 commit comments

Comments
 (0)