Skip to content

Commit 33e0f94

Browse files
committed
Demonstrate useMemo behavior with setState during render
1 parent d5ddc65 commit 33e0f94

1 file changed

Lines changed: 89 additions & 0 deletions

File tree

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
let React;
2+
let ReactNoop;
3+
let act;
4+
let useState;
5+
6+
describe('possible useMemo invalidation bug', () => {
7+
beforeEach(() => {
8+
jest.resetModules();
9+
10+
React = require('react');
11+
ReactNoop = require('react-noop-renderer');
12+
act = require('jest-react').act;
13+
useState = React.useState;
14+
});
15+
16+
// @gate experimental || www
17+
test('caches values whose inputs are unchanged after setstate in render (useMemo)', async () => {
18+
let setX;
19+
function Component({limit}) {
20+
const [x, _setX] = useState(0);
21+
setX = _setX;
22+
23+
// `x` is a controlled state that can't be set higher than the provided `limit`
24+
if (x > limit) {
25+
setX(limit);
26+
}
27+
28+
// `obj` is an object that captures the value of `x`
29+
const obj = React.useMemo(
30+
// POSSIBLE BUG: because useMemo tries to reuse the WIP memo value after a setState-in-render,
31+
// the previous value is discarded. This means that even though the final render has the same
32+
// inputs as the last commit, the memoized value is discarded and recreated, breaking
33+
// memoization of all the child components down the tree.
34+
//
35+
// 1) First render: cache is initialized to {count: 0} with deps of [x=0, limit=10]
36+
// 2) Update: cache updated to {count: 10} with deps of [x=10, limit=10]
37+
// 3) Second update:
38+
// 3a) initially renders and caches {count: 12} with deps of [x=12, limit=10]
39+
// 3b) re-renders bc of setstate, caches {count: 10} with deps of [x=10, limit=10]
40+
// If this last step started from the `current` fiber's memo cache (from 2, instead of from 3a),
41+
// then it would not re-execute and preserve object identity
42+
() => {
43+
return {count: x};
44+
},
45+
// Note that `limit` isn't technically a dependency,
46+
// it's included here to show that even if we modeled that
47+
// `x` can depend on the value of `limit`, it isn't sufficient
48+
// to avoid breaking memoization across renders
49+
[x, limit],
50+
);
51+
52+
return <Child obj={obj} />;
53+
}
54+
55+
const Child = React.memo(function Child({obj}) {
56+
const text = React.useMemo(() => {
57+
return {text: `Count ${obj.count}`};
58+
}, [obj]);
59+
return <Text value={text} />;
60+
});
61+
62+
// Text should only ever re-render if the object identity of `value`
63+
// changes.
64+
let renderCount = 0;
65+
const Text = React.memo(function Text({value}) {
66+
renderCount++;
67+
return value.text;
68+
});
69+
70+
const root = ReactNoop.createRoot();
71+
await act(async () => {
72+
root.render(<Component limit={10} />);
73+
});
74+
expect(root).toMatchRenderedOutput('Count 0');
75+
expect(renderCount).toBe(1);
76+
77+
await act(async () => {
78+
setX(10); // set to the limit
79+
});
80+
expect(root).toMatchRenderedOutput('Count 10');
81+
expect(renderCount).toBe(2);
82+
83+
await act(async () => {
84+
setX(12); // exceeds limit, will be reset in setState during render
85+
});
86+
expect(root).toMatchRenderedOutput('Count 10');
87+
expect(renderCount).toBe(2); // should not re-render, since value has not changed
88+
});
89+
});

0 commit comments

Comments
 (0)