48
});
49
}
50
51
+ it('bails out in the render phase if all of the state is the same', () => {
52
+ const {useState, useLayoutEffect} = React;
53
+
54
+ function Child({text}) {
55
+ ReactTestRenderer.unstable_yield('Child: ' + text);
56
+ return text;
57
+ }
58
+
59
+ let setCounter1;
60
+ let setCounter2;
61
+ function Parent() {
62
+ const [counter1, _setCounter1] = useState(0);
63
+ setCounter1 = _setCounter1;
64
+ const [counter2, _setCounter2] = useState(0);
65
+ setCounter2 = _setCounter2;
66
+
67
+ const text = `${counter1}, ${counter2}`;
68
+ ReactTestRenderer.unstable_yield(`Parent: ${text}`);
69
+ useLayoutEffect(() => {
70
+ ReactTestRenderer.unstable_yield(`Effect: ${text}`);
71
+ });
72
+ return <Child text={text} />;
73
+ }
74
+
75
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
76
+ root.update(<Parent />);
77
+ expect(root).toFlushAndYield([
78
+ 'Parent: 0, 0',
79
+ 'Child: 0, 0',
80
+ 'Effect: 0, 0',
81
+ ]);
82
+ expect(root).toMatchRenderedOutput('0, 0');
83
+
84
+ // Normal update
85
+ setCounter1(1);
86
+ setCounter2(1);
87
+ expect(root).toFlushAndYield([
88
+ 'Parent: 1, 1',
89
+ 'Child: 1, 1',
90
+ 'Effect: 1, 1',
91
+ ]);
92
+
93
+ // Update that bails out.
94
+ setCounter1(1);
95
+ expect(root).toFlushAndYield(['Parent: 1, 1']);
96
+
97
+ // This time, one of the state updates but the other one doesn't. So we
98
+ // can't bail out.
99
+ setCounter1(1);
100
+ setCounter2(2);
101
+ expect(root).toFlushAndYield([
102
+ 'Parent: 1, 2',
103
+ 'Child: 1, 2',
104
+ 'Effect: 1, 2',
105
+ ]);
106
+
107
+ // Lots of updates that eventually resolve to the current values.
108
+ setCounter1(9);
109
+ setCounter2(3);
110
+ setCounter1(4);
111
+ setCounter2(7);
112
+ setCounter1(1);
113
+ setCounter2(2);
114
+
115
+ // Because the final values are the same as the current values, the
116
+ // component bails out.
117
+ expect(root).toFlushAndYield(['Parent: 1, 2']);
118
+ });
119
+
120
+ it('bails out in render phase if all the state is the same and props bail out with memo', () => {
121
+ const {useState, memo} = React;
122
+
123
+ function Child({text}) {
124
+ ReactTestRenderer.unstable_yield('Child: ' + text);
125
+ return text;
126
+ }
127
+
128
+ let setCounter1;
129
+ let setCounter2;
130
+ function Parent({theme}) {
131
+ const [counter1, _setCounter1] = useState(0);
132
+ setCounter1 = _setCounter1;
133
+ const [counter2, _setCounter2] = useState(0);
134
+ setCounter2 = _setCounter2;
135
+
136
+ const text = `${counter1}, ${counter2} (${theme})`;
137
+ ReactTestRenderer.unstable_yield(`Parent: ${text}`);
138
+ return <Child text={text} />;
139
+ }
140
+
141
+ Parent = memo(Parent);
142
+
143
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
144
+ root.update(<Parent theme="light" />);
145
+ expect(root).toFlushAndYield([
146
+ 'Parent: 0, 0 (light)',
147
+ 'Child: 0, 0 (light)',
148
+ ]);
149
+ expect(root).toMatchRenderedOutput('0, 0 (light)');
150
+
151
+ // Normal update
152
+ setCounter1(1);
153
+ setCounter2(1);
154
+ expect(root).toFlushAndYield([
155
+ 'Parent: 1, 1 (light)',
156
+ 'Child: 1, 1 (light)',
157
+ ]);
158
+
159
+ // Update that bails out.
160
+ setCounter1(1);
161
+ expect(root).toFlushAndYield(['Parent: 1, 1 (light)']);
162
+
163
+ // This time, one of the state updates but the other one doesn't. So we
164
+ // can't bail out.
165
+ setCounter1(1);
166
+ setCounter2(2);
167
+ expect(root).toFlushAndYield([
168
+ 'Parent: 1, 2 (light)',
169
+ 'Child: 1, 2 (light)',
170
+ ]);
171
+
172
+ // Updates bail out, but component still renders because props
173
+ // have changed
174
+ setCounter1(1);
175
+ setCounter2(2);
176
+ root.update(<Parent theme="dark" />);
177
+ expect(root).toFlushAndYield(['Parent: 1, 2 (dark)', 'Child: 1, 2 (dark)']);
178
+
179
+ // Both props and state bail out
180
+ setCounter1(1);
181
+ setCounter2(2);
182
+ root.update(<Parent theme="dark" />);
183
+ expect(root).toFlushAndYield(['Parent: 1, 2 (dark)']);
184
+ });
185
+
186
+ it('never bails out if context has changed', () => {
187
+ const {useState, useLayoutEffect, useContext} = React;
188
+
189
+ const ThemeContext = React.createContext('light');
190
+
191
+ let setTheme;
192
+ function ThemeProvider({children}) {
193
+ const [theme, _setTheme] = useState('light');
194
+ ReactTestRenderer.unstable_yield('Theme: ' + theme);
195
+ setTheme = _setTheme;
196
+ return (
197
+ <ThemeContext.Provider value={theme}>{children}</ThemeContext.Provider>
198
+ );
199
+ }
200
+
201
+ function Child({text}) {
202
+ ReactTestRenderer.unstable_yield('Child: ' + text);
203
+ return text;
204
+ }
205
+
206
+ let setCounter;
207
+ function Parent() {
208
+ const [counter, _setCounter] = useState(0);
209
+ setCounter = _setCounter;
210
+
211
+ const theme = useContext(ThemeContext);
212
+
213
+ const text = `${counter} (${theme})`;
214
+ ReactTestRenderer.unstable_yield(`Parent: ${text}`);
215
+ useLayoutEffect(() => {
216
+ ReactTestRenderer.unstable_yield(`Effect: ${text}`);
217
+ });
218
+ return <Child text={text} />;
219
+ }
220
+
221
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
222
+ root.update(
223
+ <ThemeProvider>
224
+ <Parent />
225
+ </ThemeProvider>,
226
+ );
227
+ expect(root).toFlushAndYield([
228
+ 'Theme: light',
229
+ 'Parent: 0 (light)',
230
+ 'Child: 0 (light)',
231
+ 'Effect: 0 (light)',
232
+ ]);
233
+ expect(root).toMatchRenderedOutput('0 (light)');
234
+
235
+ // Updating the theme to the same value does't cause the consumers
236
+ // to re-render.
237
+ setTheme('light');
238
+ expect(root).toFlushAndYield([]);
239
+ expect(root).toMatchRenderedOutput('0 (light)');
240
+
241
+ // Normal update
242
+ setCounter(1);
243
+ expect(root).toFlushAndYield([
244
+ 'Parent: 1 (light)',
245
+ 'Child: 1 (light)',
246
+ 'Effect: 1 (light)',
247
+ ]);
248
+ expect(root).toMatchRenderedOutput('1 (light)');
249
+
250
+ // Update that doesn't change state, so it bails out
251
+ setCounter(1);
252
+ expect(root).toFlushAndYield(['Parent: 1 (light)']);
253
+ expect(root).toMatchRenderedOutput('1 (light)');
254
+
255
+ // Update that doesn't change state, but the context changes, too, so it
256
+ // can't bail out
257
+ setCounter(1);
258
+ setTheme('dark');
259
+ expect(root).toFlushAndYield([
260
+ 'Theme: dark',
261
+ 'Parent: 1 (dark)',
262
+ 'Child: 1 (dark)',
263
+ 'Effect: 1 (dark)',
264
+ ]);
265
+ expect(root).toMatchRenderedOutput('1 (dark)');
266
+ });
267
+
268
+ it('can bail out without calling render phase (as an optimization) if queue is known to be empty', () => {
269
+ const {useState, useLayoutEffect} = React;
270
+
271
+ function Child({text}) {
272
+ ReactTestRenderer.unstable_yield('Child: ' + text);
273
+ return text;
274
+ }
275
+
276
+ let setCounter;
277
+ function Parent() {
278
+ const [counter, _setCounter] = useState(0);
279
+ setCounter = _setCounter;
280
+ ReactTestRenderer.unstable_yield('Parent: ' + counter);
281
+ useLayoutEffect(() => {
282
+ ReactTestRenderer.unstable_yield('Effect: ' + counter);
283
+ });
284
+ return <Child text={counter} />;
285
+ }
286
+
287
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
288
+ root.update(<Parent />);
289
+ expect(root).toFlushAndYield(['Parent: 0', 'Child: 0', 'Effect: 0']);
290
+ expect(root).toMatchRenderedOutput('0');
291
+
292
+ // Normal update
293
+ setCounter(1);
294
+ expect(root).toFlushAndYield(['Parent: 1', 'Child: 1', 'Effect: 1']);
295
+ expect(root).toMatchRenderedOutput('1');
296
+
297
+ // Update to the same state. React doesn't know if the queue is empty
298
+ // because the alterate fiber has pending update priority, so we have to
299
+ // enter the render phase before we can bail out. But we bail out before
300
+ // rendering the child, and we don't fire any effects.
301
+ setCounter(1);
302
+ expect(root).toFlushAndYield(['Parent: 1']);
303
+ expect(root).toMatchRenderedOutput('1');
304
+
305
+ // Update to the same state again. This times, neither fiber has pending
306
+ // update priority, so we can bail out before even entering the render phase.
307
+ setCounter(1);
308
+ expect(root).toFlushAndYield([]);
309
+ expect(root).toMatchRenderedOutput('1');
310
+
311
+ // This changes the state to something different so it renders normally.
312
+ setCounter(2);
313
+ expect(root).toFlushAndYield(['Parent: 2', 'Child: 2', 'Effect: 2']);
314
+ expect(root).toMatchRenderedOutput('2');
315
+ });
316
+
317
+ it('bails out multiple times in a row without entering render phase', () => {
318
+ const {useState} = React;
319
+
320
+ function Child({text}) {
321
+ ReactTestRenderer.unstable_yield('Child: ' + text);
322
+ return text;
323
+ }
324
+
325
+ let setCounter;
326
+ function Parent() {
327
+ const [counter, _setCounter] = useState(0);
328
+ setCounter = _setCounter;
329
+ ReactTestRenderer.unstable_yield('Parent: ' + counter);
330
+ return <Child text={counter} />;
331
+ }
332
+
333
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
334
+ root.update(<Parent />);
335
+ expect(root).toFlushAndYield(['Parent: 0', 'Child: 0']);
336
+ expect(root).toMatchRenderedOutput('0');
337
+
338
+ const update = value => {
339
+ setCounter(previous => {
340
+ ReactTestRenderer.unstable_yield(
341
+ `Compute state (${previous} -> ${value})`,
342
+ );
343
+ return value;
344
+ });
345
+ };
346
+ update(0);
347
+ update(0);
348
+ update(0);
349
+ update(1);
350
+ update(2);
351
+ update(3);
352
+
353
+ expect(ReactTestRenderer).toHaveYielded([
354
+ // The first four updates were eagerly computed, because the queue is
355
+ // empty before each one.
356
+ 'Compute state (0 -> 0)',
357
+ 'Compute state (0 -> 0)',
358
+ 'Compute state (0 -> 0)',
359
+ // The fourth update doesn't bail out
360
+ 'Compute state (0 -> 1)',
361
+ // so subsequent updates can't be eagerly computed.
362
+ ]);
363
+
364
+ // Now let's enter the render phase
365
+ expect(root).toFlushAndYield([
366
+ // We don't need to re-compute the first four updates. Only the final two.
367
+ 'Compute state (1 -> 2)',
368
+ 'Compute state (2 -> 3)',
369
+ 'Parent: 3',
370
+ 'Child: 3',
371
+ ]);
372
+ expect(root).toMatchRenderedOutput('3');
373
+ });
374
+
375
+ it('can rebase on top of a previously skipped update', () => {
376
+ const {useState} = React;
377
+
378
+ function Child({text}) {
379
+ ReactTestRenderer.unstable_yield('Child: ' + text);
380
+ return text;
381
+ }
382
+
383
+ let setCounter;
384
+ function Parent() {
385
+ const [counter, _setCounter] = useState(1);
386
+ setCounter = _setCounter;
387
+ ReactTestRenderer.unstable_yield('Parent: ' + counter);
388
+ return <Child text={counter} />;
389
+ }
390
+
391
+ const root = ReactTestRenderer.create(null, {unstable_isConcurrent: true});
392
+ root.update(<Parent />);
393
+ expect(root).toFlushAndYield(['Parent: 1', 'Child: 1']);
394
+ expect(root).toMatchRenderedOutput('1');
395
+
396
+ const update = compute => {
397
+ setCounter(previous => {
398
+ const value = compute(previous);
399
+ ReactTestRenderer.unstable_yield(
400
+ `Compute state (${previous} -> ${value})`,
401
+ );
402
+ return value;
403
+ });
404
+ };
405
+
406
+ // Update at normal priority
407
+ update(n => n * 100);
408
+
409
+ // The new state is eagerly computed.
410
+ expect(ReactTestRenderer).toHaveYielded(['Compute state (1 -> 100)']);
411
+
412
+ // but before it's flushed, a higher priority update interrupts it.
413
+ root.unstable_flushSync(() => {
414
+ update(n => n + 5);
415
+ });
416
+ expect(ReactTestRenderer).toHaveYielded([
417
+ // The eagerly computed state was completely skipped
418
+ 'Compute state (1 -> 6)',
419
+ 'Parent: 6',
420
+ 'Child: 6',
421
+ ]);
422
+ expect(root).toMatchRenderedOutput('6');
423
+
424
+ // Now when we finish the first update, the second update is rebased on top.
425
+ // Notice we didn't have to recompute the first update even though it was
426
+ // skipped in the previous render.
427
+ expect(root).toFlushAndYield([
428
+ 'Compute state (100 -> 105)',
429
+ 'Parent: 105',
430
+ 'Child: 105',
431
+ ]);
432
+ expect(root).toMatchRenderedOutput('105');
433
+ });
434
+
435
it('warns about variable number of dependencies', () => {
436
const {useLayoutEffect} = React;
437
function App(props) {