Remove concurrent apis from stable (#17088)
* Tests run in experimental mode by default For local development, you usually want experiments enabled. Unless the release channel is set with an environment variable, tests will run with __EXPERIMENTAL__ set to `true`. * Remove concurrent APIs from stable builds Those who want to try concurrent mode should use the experimental builds instead. I've left the `unstable_` prefixed APIs in the Facebook build so we can continue experimenting with them internally without blessing them for widespread use. * Turn on SSR flags in experimental build * Remove prefixed concurrent APIs from www build Instead we'll use the experimental builds when syncing to www. * Remove "canary" from internal React version string
Andrew Clark committed
Oct 15, 2019 at 15:09 UTC
30c5daf943bd3bed38e464ac79e38f0e8a27426b
33 files changed
+1946
-2198
.circleci/config.yml
+58
-6
@@ -98,7 +98,10 @@ jobs:
98
- checkout
99
- *restore_yarn_cache
100
- *run_yarn
101
- - run: yarn test --maxWorkers=2
101
+ - run:
102
+ environment:
103
+ RELEASE_CHANNEL: stable
104
+ command: yarn test --maxWorkers=2
105
106
test_source_experimental:
107
docker: *docker
@@ -120,7 +123,10 @@ jobs:
123
- checkout
124
- *restore_yarn_cache
125
- *run_yarn
123
- - run: yarn test-persistent --maxWorkers=2
126
+ - run:
127
+ environment:
128
+ RELEASE_CHANNEL: stable
129
+ command: yarn test-persistent --maxWorkers=2
130
131
test_source_prod:
132
docker: *docker
@@ -130,7 +136,10 @@ jobs:
136
- checkout
137
- *restore_yarn_cache
138
- *run_yarn
133
- - run: yarn test-prod --maxWorkers=2
139
+ - run:
140
+ environment:
141
+ RELEASE_CHANNEL: stable
142
+ command: yarn test-prod --maxWorkers=2
143
144
build:
145
docker: *docker
@@ -217,7 +226,23 @@ jobs:
226
- attach_workspace: *attach_workspace
227
- *restore_yarn_cache
228
- *run_yarn
220
- - run: yarn test-build --maxWorkers=2
229
+ - run:
230
+ environment:
231
+ RELEASE_CHANNEL: stable
232
+ command: yarn test-build --maxWorkers=2
233
+
234
+ test_build_experimental:
235
+ docker: *docker
236
+ environment: *environment
237
+ steps:
238
+ - checkout
239
+ - attach_workspace: *attach_workspace
240
+ - *restore_yarn_cache
241
+ - *run_yarn
242
+ - run:
243
+ environment:
244
+ RELEASE_CHANNEL: experimental
245
+ command: yarn test-build --maxWorkers=2
246
247
test_build_devtools:
248
docker: *docker
@@ -227,7 +252,10 @@ jobs:
252
- attach_workspace: *attach_workspace
253
- *restore_yarn_cache
254
- *run_yarn
230
- - run: yarn test-build-devtools --maxWorkers=2
255
+ - run:
256
+ environment:
257
+ RELEASE_CHANNEL: stable
258
+ command: yarn test-build --maxWorkers=2
259
260
test_dom_fixtures:
261
docker: *docker
@@ -238,6 +266,8 @@ jobs:
266
- *restore_yarn_cache
267
- run:
268
name: Run DOM fixture tests
269
+ environment:
270
+ RELEASE_CHANNEL: stable
271
command: |
272
cd fixtures/dom
273
yarn --frozen-lockfile
@@ -265,7 +295,23 @@ jobs:
295
- attach_workspace: *attach_workspace
296
- *restore_yarn_cache
297
- *run_yarn
268
- - run: yarn test-build-prod --maxWorkers=2
298
+ - run:
299
+ environment:
300
+ RELEASE_CHANNEL: stable
301
+ command: yarn test-build-prod --maxWorkers=2
302
+
303
+ test_build_prod_experimental:
304
+ docker: *docker
305
+ environment: *environment
306
+ steps:
307
+ - checkout
308
+ - attach_workspace: *attach_workspace
309
+ - *restore_yarn_cache
310
+ - *run_yarn
311
+ - run:
312
+ environment:
313
+ RELEASE_CHANNEL: experimental
314
+ command: yarn test-build-prod --maxWorkers=2
315
316
workflows:
317
version: 2
@@ -324,6 +370,12 @@ workflows:
370
- process_artifacts_experimental:
371
requires:
372
- build_experimental
373
+ - test_build_experimental:
374
+ requires:
375
+ - build_experimental
376
+ - test_build_prod_experimental:
377
+ requires:
378
+ - build_experimental
379
380
fuzz_tests:
381
triggers:
fixtures/dom/src/__tests__/wrong-act-test.js
+19
-16
@@ -17,6 +17,7 @@ let TestRenderer;
17
let ARTTest;
18
19
global.__DEV__ = process.env.NODE_ENV !== 'production';
20
+global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
21
22
expect.extend(require('../toWarnDev'));
23
@@ -176,19 +177,21 @@ it("doesn't warn if you use nested acts from different renderers", () => {
177
});
178
});
179
179
-it('warns when using createRoot() + .render', () => {
180
- const root = ReactDOM.unstable_createRoot(document.createElement('div'));
181
- expect(() => {
182
- TestRenderer.act(() => {
183
- root.render(<App />);
184
- });
185
- }).toWarnDev(
186
- [
187
- 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
188
- "It looks like you're using the wrong act()",
189
- ],
190
- {
191
- withoutStack: true,
192
- }
193
- );
194
-});
180
+if (__EXPERIMENTAL__) {
181
+ it('warns when using createRoot() + .render', () => {
182
+ const root = ReactDOM.createRoot(document.createElement('div'));
183
+ expect(() => {
184
+ TestRenderer.act(() => {
185
+ root.render(<App />);
186
+ });
187
+ }).toWarnDev(
188
+ [
189
+ 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked',
190
+ "It looks like you're using the wrong act()",
191
+ ],
192
+ {
193
+ withoutStack: true,
194
+ }
195
+ );
196
+ });
197
+}
packages/react-devtools-shared/src/__tests__/__snapshots__/storeStressTestConcurrent-test.js.snap
deleted
-493
@@ -1,493 +0,0 @@
1
-// Jest Snapshot v1, https://goo.gl/fbAQLP
2
-
3
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 1`] = `
4
-[root]
5
- ▾ <Root>
6
- <X>
7
- ▾ <Suspense>
8
- <A key="a">
9
- <Y>
10
-`;
11
-
12
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 2`] = `
13
-[root]
14
- ▾ <Root>
15
- <X>
16
- ▾ <Suspense>
17
- <A key="a">
18
- <Y>
19
-`;
20
-
21
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 3`] = `
22
-[root]
23
- ▾ <Root>
24
- <X>
25
- ▾ <Suspense>
26
- <A key="a">
27
- <B key="b">
28
- <C key="c">
29
- <Y>
30
-`;
31
-
32
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 4`] = `
33
-[root]
34
- ▾ <Root>
35
- <X>
36
- ▾ <Suspense>
37
- <C key="c">
38
- <B key="b">
39
- <A key="a">
40
- <Y>
41
-`;
42
-
43
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 5`] = `
44
-[root]
45
- ▾ <Root>
46
- <X>
47
- ▾ <Suspense>
48
- <C key="c">
49
- <A key="a">
50
- <Y>
51
-`;
52
-
53
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 6`] = `
54
-[root]
55
- ▾ <Root>
56
- <X>
57
- ▾ <Suspense>
58
- <C key="c">
59
- <A key="a">
60
- <Y>
61
-`;
62
-
63
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 7`] = `
64
-[root]
65
- ▾ <Root>
66
- <X>
67
- ▾ <Suspense>
68
- <C key="c">
69
- <A key="a">
70
- <Y>
71
-`;
72
-
73
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 8`] = `
74
-[root]
75
- ▾ <Root>
76
- <X>
77
- ▾ <Suspense>
78
- <A key="a">
79
- <B key="b">
80
- <Y>
81
-`;
82
-
83
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 9`] = `
84
-[root]
85
- ▾ <Root>
86
- <X>
87
- ▾ <Suspense>
88
- <A key="a">
89
- <Y>
90
-`;
91
-
92
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 10`] = `
93
-[root]
94
- ▾ <Root>
95
- <X>
96
- <Suspense>
97
- <Y>
98
-`;
99
-
100
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 11`] = `
101
-[root]
102
- ▾ <Root>
103
- <X>
104
- ▾ <Suspense>
105
- <B key="b">
106
- <Y>
107
-`;
108
-
109
-exports[`StoreStressConcurrent should handle a stress test for Suspense (Concurrent Mode) 12`] = `
110
-[root]
111
- ▾ <Root>
112
- <X>
113
- ▾ <Suspense>
114
- <A key="a">
115
- <Y>
116
-`;
117
-
118
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 1`] = `
119
-[root]
120
- ▾ <Root>
121
- <X>
122
- ▾ <Suspense>
123
- ▾ <MaybeSuspend>
124
- <A key="a">
125
- <Z>
126
- <Y>
127
-`;
128
-
129
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 2`] = `
130
-[root]
131
- ▾ <Root>
132
- <X>
133
- ▾ <Suspense>
134
- ▾ <MaybeSuspend>
135
- <A key="a">
136
- <Z>
137
- <Y>
138
-`;
139
-
140
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 3`] = `
141
-[root]
142
- ▾ <Root>
143
- <X>
144
- ▾ <Suspense>
145
- ▾ <MaybeSuspend>
146
- <A key="a">
147
- <B key="b">
148
- <C key="c">
149
- <Z>
150
- <Y>
151
-`;
152
-
153
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 4`] = `
154
-[root]
155
- ▾ <Root>
156
- <X>
157
- ▾ <Suspense>
158
- ▾ <MaybeSuspend>
159
- <C key="c">
160
- <B key="b">
161
- <A key="a">
162
- <Z>
163
- <Y>
164
-`;
165
-
166
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 5`] = `
167
-[root]
168
- ▾ <Root>
169
- <X>
170
- ▾ <Suspense>
171
- ▾ <MaybeSuspend>
172
- <C key="c">
173
- <A key="a">
174
- <Z>
175
- <Y>
176
-`;
177
-
178
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 6`] = `
179
-[root]
180
- ▾ <Root>
181
- <X>
182
- ▾ <Suspense>
183
- ▾ <MaybeSuspend>
184
- <C key="c">
185
- <A key="a">
186
- <Z>
187
- <Y>
188
-`;
189
-
190
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 7`] = `
191
-[root]
192
- ▾ <Root>
193
- <X>
194
- ▾ <Suspense>
195
- ▾ <MaybeSuspend>
196
- <C key="c">
197
- <A key="a">
198
- <Z>
199
- <Y>
200
-`;
201
-
202
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 8`] = `
203
-[root]
204
- ▾ <Root>
205
- <X>
206
- ▾ <Suspense>
207
- ▾ <MaybeSuspend>
208
- <A key="a">
209
- <B key="b">
210
- <Z>
211
- <Y>
212
-`;
213
-
214
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 9`] = `
215
-[root]
216
- ▾ <Root>
217
- <X>
218
- ▾ <Suspense>
219
- ▾ <MaybeSuspend>
220
- <A key="a">
221
- <Z>
222
- <Y>
223
-`;
224
-
225
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 10`] = `
226
-[root]
227
- ▾ <Root>
228
- <X>
229
- ▾ <Suspense>
230
- ▾ <MaybeSuspend>
231
- <Z>
232
- <Y>
233
-`;
234
-
235
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 11`] = `
236
-[root]
237
- ▾ <Root>
238
- <X>
239
- ▾ <Suspense>
240
- ▾ <MaybeSuspend>
241
- <B key="b">
242
- <Z>
243
- <Y>
244
-`;
245
-
246
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 12`] = `
247
-[root]
248
- ▾ <Root>
249
- <X>
250
- ▾ <Suspense>
251
- ▾ <MaybeSuspend>
252
- <A key="a">
253
- <Z>
254
- <Y>
255
-`;
256
-
257
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 13`] = `
258
-[root]
259
- ▾ <Root>
260
- <X>
261
- ▾ <Suspense>
262
- <A key="a">
263
- <Y>
264
-`;
265
-
266
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 14`] = `
267
-[root]
268
- ▾ <Root>
269
- <X>
270
- ▾ <Suspense>
271
- <A key="a">
272
- <Y>
273
-`;
274
-
275
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 15`] = `
276
-[root]
277
- ▾ <Root>
278
- <X>
279
- ▾ <Suspense>
280
- <A key="a">
281
- <B key="b">
282
- <C key="c">
283
- <Y>
284
-`;
285
-
286
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 16`] = `
287
-[root]
288
- ▾ <Root>
289
- <X>
290
- ▾ <Suspense>
291
- <C key="c">
292
- <B key="b">
293
- <A key="a">
294
- <Y>
295
-`;
296
-
297
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 17`] = `
298
-[root]
299
- ▾ <Root>
300
- <X>
301
- ▾ <Suspense>
302
- <C key="c">
303
- <A key="a">
304
- <Y>
305
-`;
306
-
307
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 18`] = `
308
-[root]
309
- ▾ <Root>
310
- <X>
311
- ▾ <Suspense>
312
- <C key="c">
313
- <A key="a">
314
- <Y>
315
-`;
316
-
317
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 19`] = `
318
-[root]
319
- ▾ <Root>
320
- <X>
321
- ▾ <Suspense>
322
- <C key="c">
323
- <A key="a">
324
- <Y>
325
-`;
326
-
327
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 20`] = `
328
-[root]
329
- ▾ <Root>
330
- <X>
331
- ▾ <Suspense>
332
- <A key="a">
333
- <B key="b">
334
- <Y>
335
-`;
336
-
337
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 21`] = `
338
-[root]
339
- ▾ <Root>
340
- <X>
341
- ▾ <Suspense>
342
- <A key="a">
343
- <Y>
344
-`;
345
-
346
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 22`] = `
347
-[root]
348
- ▾ <Root>
349
- <X>
350
- <Suspense>
351
- <Y>
352
-`;
353
-
354
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 23`] = `
355
-[root]
356
- ▾ <Root>
357
- <X>
358
- ▾ <Suspense>
359
- <B key="b">
360
- <Y>
361
-`;
362
-
363
-exports[`StoreStressConcurrent should handle a stress test for Suspense without type change (Concurrent Mode) 24`] = `
364
-[root]
365
- ▾ <Root>
366
- <X>
367
- ▾ <Suspense>
368
- <A key="a">
369
- <Y>
370
-`;
371
-
372
-exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 1: abcde 1`] = `
373
-[root]
374
- ▾ <Parent>
375
- <A key="a">
376
- <B key="b">
377
- <C key="c">
378
- <D key="d">
379
- <E key="e">
380
-`;
381
-
382
-exports[`StoreStressConcurrent should handle a stress test with different tree operations (Concurrent Mode): 2: abxde 1`] = `
383
-[root]
384
- ▾ <Parent>
385
- <A key="a">
386
- <B key="b">
387
- ▾ <C key="c">
388
- <X>
389
- <D key="d">
390
- <E key="e">
391
-`;
392
-
393
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 1`] = `
394
-[root]
395
- ▾ <Root>
396
- <A key="a">
397
-`;
398
-
399
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 2`] = `
400
-[root]
401
- ▾ <Root>
402
- <B key="b">
403
-`;
404
-
405
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 3`] = `
406
-[root]
407
- ▾ <Root>
408
- <C key="c">
409
-`;
410
-
411
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 4`] = `
412
-[root]
413
- ▾ <Root>
414
- <D key="d">
415
-`;
416
-
417
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 5`] = `
418
-[root]
419
- ▾ <Root>
420
- <E key="e">
421
-`;
422
-
423
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 6`] = `
424
-[root]
425
- ▾ <Root>
426
- <A key="a">
427
-`;
428
-
429
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 7`] = `
430
-[root]
431
- ▾ <Root>
432
- <B key="b">
433
-`;
434
-
435
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 8`] = `
436
-[root]
437
- ▾ <Root>
438
- <C key="c">
439
-`;
440
-
441
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 9`] = `
442
-[root]
443
- ▾ <Root>
444
- <D key="d">
445
-`;
446
-
447
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 10`] = `
448
-[root]
449
- ▾ <Root>
450
- <E key="e">
451
-`;
452
-
453
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 11`] = `
454
-[root]
455
- ▾ <Root>
456
- <A key="a">
457
- <B key="b">
458
-`;
459
-
460
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 12`] = `
461
-[root]
462
- ▾ <Root>
463
- <B key="b">
464
- <A key="a">
465
-`;
466
-
467
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 13`] = `
468
-[root]
469
- ▾ <Root>
470
- <B key="b">
471
- <C key="c">
472
-`;
473
-
474
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 14`] = `
475
-[root]
476
- ▾ <Root>
477
- <C key="c">
478
- <B key="b">
479
-`;
480
-
481
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 15`] = `
482
-[root]
483
- ▾ <Root>
484
- <A key="a">
485
- <C key="c">
486
-`;
487
-
488
-exports[`StoreStressConcurrent should handle stress test with reordering (Concurrent Mode) 16`] = `
489
-[root]
490
- ▾ <Root>
491
- <C key="c">
492
- <A key="a">
493
-`;
packages/react-devtools-shared/src/__tests__/storeStressTestConcurrent-test.js
+298
-25
@@ -27,6 +27,11 @@ describe('StoreStressConcurrent', () => {
27
print = require('./storeSerializer').print;
28
});
29
30
+ if (!__EXPERIMENTAL__) {
31
+ it("empty test so Jest doesn't complain", () => {});
32
+ return;
33
+ }
34
+
35
// This is a stress test for the tree mount/update/unmount traversal.
36
// It renders different trees that should produce the same output.
37
it('should handle a stress test with different tree operations (Concurrent Mode)', () => {
@@ -57,9 +62,19 @@ describe('StoreStressConcurrent', () => {
62
// 1. Render a normal version of [a, b, c, d, e].
63
let container = document.createElement('div');
64
// $FlowFixMe
60
- let root = ReactDOM.unstable_createRoot(container);
65
+ let root = ReactDOM.createRoot(container);
66
act(() => root.render(<Parent>{[a, b, c, d, e]}</Parent>));
62
- expect(store).toMatchSnapshot('1: abcde');
67
+ expect(store).toMatchInlineSnapshot(
68
+ `
69
+ [root]
70
+ ▾ <Parent>
71
+ <A key="a">
72
+ <B key="b">
73
+ <C key="c">
74
+ <D key="d">
75
+ <E key="e">
76
+ `,
77
+ );
78
expect(container.textContent).toMatch('abcde');
79
const snapshotForABCDE = print(store);
80
@@ -68,7 +83,18 @@ describe('StoreStressConcurrent', () => {
83
act(() => {
84
setShowX(true);
85
});
71
- expect(store).toMatchSnapshot('2: abxde');
86
+ expect(store).toMatchInlineSnapshot(
87
+ `
88
+ [root]
89
+ ▾ <Parent>
90
+ <A key="a">
91
+ <B key="b">
92
+ ▾ <C key="c">
93
+ <X>
94
+ <D key="d">
95
+ <E key="e">
96
+ `,
97
+ );
98
expect(container.textContent).toMatch('abxde');
99
const snapshotForABXDE = print(store);
100
@@ -120,7 +146,7 @@ describe('StoreStressConcurrent', () => {
146
// Ensure fresh mount.
147
container = document.createElement('div');
148
// $FlowFixMe
123
- root = ReactDOM.unstable_createRoot(container);
149
+ root = ReactDOM.createRoot(container);
150
151
// Verify mounting 'abcde'.
152
act(() => root.render(<Parent>{cases[i]}</Parent>));
@@ -150,7 +176,7 @@ describe('StoreStressConcurrent', () => {
176
// There'll be no unmounting until the very end.
177
container = document.createElement('div');
178
// $FlowFixMe
153
- root = ReactDOM.unstable_createRoot(container);
179
+ root = ReactDOM.createRoot(container);
180
for (let i = 0; i < cases.length; i++) {
181
// Verify mounting 'abcde'.
182
act(() => root.render(<Parent>{cases[i]}</Parent>));
@@ -216,22 +242,80 @@ describe('StoreStressConcurrent', () => {
242
let snapshots = [];
243
let container = document.createElement('div');
244
// $FlowFixMe
219
- let root = ReactDOM.unstable_createRoot(container);
245
+ let root = ReactDOM.createRoot(container);
246
for (let i = 0; i < steps.length; i++) {
247
act(() => root.render(<Root>{steps[i]}</Root>));
248
// We snapshot each step once so it doesn't regress.
223
- expect(store).toMatchSnapshot();
249
snapshots.push(print(store));
250
act(() => root.unmount());
251
expect(print(store)).toBe('');
252
}
253
254
+ expect(snapshots).toMatchInlineSnapshot(`
255
+ Array [
256
+ "[root]
257
+ ▾ <Root>
258
+ <A key=\\"a\\">",
259
+ "[root]
260
+ ▾ <Root>
261
+ <B key=\\"b\\">",
262
+ "[root]
263
+ ▾ <Root>
264
+ <C key=\\"c\\">",
265
+ "[root]
266
+ ▾ <Root>
267
+ <D key=\\"d\\">",
268
+ "[root]
269
+ ▾ <Root>
270
+ <E key=\\"e\\">",
271
+ "[root]
272
+ ▾ <Root>
273
+ <A key=\\"a\\">",
274
+ "[root]
275
+ ▾ <Root>
276
+ <B key=\\"b\\">",
277
+ "[root]
278
+ ▾ <Root>
279
+ <C key=\\"c\\">",
280
+ "[root]
281
+ ▾ <Root>
282
+ <D key=\\"d\\">",
283
+ "[root]
284
+ ▾ <Root>
285
+ <E key=\\"e\\">",
286
+ "[root]
287
+ ▾ <Root>
288
+ <A key=\\"a\\">
289
+ <B key=\\"b\\">",
290
+ "[root]
291
+ ▾ <Root>
292
+ <B key=\\"b\\">
293
+ <A key=\\"a\\">",
294
+ "[root]
295
+ ▾ <Root>
296
+ <B key=\\"b\\">
297
+ <C key=\\"c\\">",
298
+ "[root]
299
+ ▾ <Root>
300
+ <C key=\\"c\\">
301
+ <B key=\\"b\\">",
302
+ "[root]
303
+ ▾ <Root>
304
+ <A key=\\"a\\">
305
+ <C key=\\"c\\">",
306
+ "[root]
307
+ ▾ <Root>
308
+ <C key=\\"c\\">
309
+ <A key=\\"a\\">",
310
+ ]
311
+ `);
312
+
313
// 2. Verify that we can update from every step to every other step and back.
314
for (let i = 0; i < steps.length; i++) {
315
for (let j = 0; j < steps.length; j++) {
316
container = document.createElement('div');
317
// $FlowFixMe
234
- root = ReactDOM.unstable_createRoot(container);
318
+ root = ReactDOM.createRoot(container);
319
act(() => root.render(<Root>{steps[i]}</Root>));
320
expect(print(store)).toMatch(snapshots[i]);
321
act(() => root.render(<Root>{steps[j]}</Root>));
@@ -248,7 +332,7 @@ describe('StoreStressConcurrent', () => {
332
for (let j = 0; j < steps.length; j++) {
333
container = document.createElement('div');
334
// $FlowFixMe
251
- root = ReactDOM.unstable_createRoot(container);
335
+ root = ReactDOM.createRoot(container);
336
act(() =>
337
root.render(
338
<Root>
@@ -320,7 +404,7 @@ describe('StoreStressConcurrent', () => {
404
let snapshots = [];
405
let container = document.createElement('div');
406
// $FlowFixMe
323
- let root = ReactDOM.unstable_createRoot(container);
407
+ let root = ReactDOM.createRoot(container);
408
for (let i = 0; i < steps.length; i++) {
409
act(() =>
410
root.render(
@@ -331,13 +415,96 @@ describe('StoreStressConcurrent', () => {
415
</Root>,
416
),
417
);
334
- // We snapshot each step once so it doesn't regress.
335
- expect(store).toMatchSnapshot();
418
+ // We snapshot each step once so it doesn't regress.d
419
snapshots.push(print(store));
420
act(() => root.unmount());
421
expect(print(store)).toBe('');
422
}
423
424
+ expect(snapshots).toMatchInlineSnapshot(`
425
+ Array [
426
+ "[root]
427
+ ▾ <Root>
428
+ <X>
429
+ ▾ <Suspense>
430
+ <A key=\\"a\\">
431
+ <Y>",
432
+ "[root]
433
+ ▾ <Root>
434
+ <X>
435
+ ▾ <Suspense>
436
+ <A key=\\"a\\">
437
+ <Y>",
438
+ "[root]
439
+ ▾ <Root>
440
+ <X>
441
+ ▾ <Suspense>
442
+ <A key=\\"a\\">
443
+ <B key=\\"b\\">
444
+ <C key=\\"c\\">
445
+ <Y>",
446
+ "[root]
447
+ ▾ <Root>
448
+ <X>
449
+ ▾ <Suspense>
450
+ <C key=\\"c\\">
451
+ <B key=\\"b\\">
452
+ <A key=\\"a\\">
453
+ <Y>",
454
+ "[root]
455
+ ▾ <Root>
456
+ <X>
457
+ ▾ <Suspense>
458
+ <C key=\\"c\\">
459
+ <A key=\\"a\\">
460
+ <Y>",
461
+ "[root]
462
+ ▾ <Root>
463
+ <X>
464
+ ▾ <Suspense>
465
+ <C key=\\"c\\">
466
+ <A key=\\"a\\">
467
+ <Y>",
468
+ "[root]
469
+ ▾ <Root>
470
+ <X>
471
+ ▾ <Suspense>
472
+ <C key=\\"c\\">
473
+ <A key=\\"a\\">
474
+ <Y>",
475
+ "[root]
476
+ ▾ <Root>
477
+ <X>
478
+ ▾ <Suspense>
479
+ <A key=\\"a\\">
480
+ <B key=\\"b\\">
481
+ <Y>",
482
+ "[root]
483
+ ▾ <Root>
484
+ <X>
485
+ ▾ <Suspense>
486
+ <A key=\\"a\\">
487
+ <Y>",
488
+ "[root]
489
+ ▾ <Root>
490
+ <X>
491
+ <Suspense>
492
+ <Y>",
493
+ "[root]
494
+ ▾ <Root>
495
+ <X>
496
+ ▾ <Suspense>
497
+ <B key=\\"b\\">
498
+ <Y>",
499
+ "[root]
500
+ ▾ <Root>
501
+ <X>
502
+ ▾ <Suspense>
503
+ <A key=\\"a\\">
504
+ <Y>",
505
+ ]
506
+ `);
507
+
508
// 2. Verify check Suspense can render same steps as initial fallback content.
509
for (let i = 0; i < steps.length; i++) {
510
act(() =>
@@ -364,7 +531,7 @@ describe('StoreStressConcurrent', () => {
531
// Always start with a fresh container and steps[i].
532
container = document.createElement('div');
533
// $FlowFixMe
367
- root = ReactDOM.unstable_createRoot(container);
534
+ root = ReactDOM.createRoot(container);
535
act(() =>
536
root.render(
537
<Root>
@@ -410,7 +577,7 @@ describe('StoreStressConcurrent', () => {
577
// Always start with a fresh container and steps[i].
578
container = document.createElement('div');
579
// $FlowFixMe
413
- root = ReactDOM.unstable_createRoot(container);
580
+ root = ReactDOM.createRoot(container);
581
act(() =>
582
root.render(
583
<Root>
@@ -468,7 +635,7 @@ describe('StoreStressConcurrent', () => {
635
// Always start with a fresh container and steps[i].
636
container = document.createElement('div');
637
// $FlowFixMe
471
- root = ReactDOM.unstable_createRoot(container);
638
+ root = ReactDOM.createRoot(container);
639
act(() =>
640
root.render(
641
<Root>
@@ -518,7 +685,7 @@ describe('StoreStressConcurrent', () => {
685
// Always start with a fresh container and steps[i].
686
container = document.createElement('div');
687
// $FlowFixMe
521
- root = ReactDOM.unstable_createRoot(container);
688
+ root = ReactDOM.createRoot(container);
689
act(() =>
690
root.render(
691
<Root>
@@ -572,7 +739,7 @@ describe('StoreStressConcurrent', () => {
739
// Always start with a fresh container and steps[i].
740
container = document.createElement('div');
741
// $FlowFixMe
575
- root = ReactDOM.unstable_createRoot(container);
742
+ root = ReactDOM.createRoot(container);
743
act(() =>
744
root.render(
745
<Root>
@@ -726,7 +893,7 @@ describe('StoreStressConcurrent', () => {
893
let snapshots = [];
894
let container = document.createElement('div');
895
// $FlowFixMe
729
- let root = ReactDOM.unstable_createRoot(container);
896
+ let root = ReactDOM.createRoot(container);
897
for (let i = 0; i < steps.length; i++) {
898
act(() =>
899
root.render(
@@ -740,7 +907,6 @@ describe('StoreStressConcurrent', () => {
907
),
908
);
909
// We snapshot each step once so it doesn't regress.
743
- expect(store).toMatchSnapshot();
910
snapshots.push(print(store));
911
act(() => root.unmount());
912
expect(print(store)).toBe('');
@@ -765,19 +931,126 @@ describe('StoreStressConcurrent', () => {
931
),
932
);
933
// We snapshot each step once so it doesn't regress.
768
- expect(store).toMatchSnapshot();
934
fallbackSnapshots.push(print(store));
935
act(() => root.unmount());
936
expect(print(store)).toBe('');
937
}
938
939
+ expect(snapshots).toMatchInlineSnapshot(`
940
+ Array [
941
+ "[root]
942
+ ▾ <Root>
943
+ <X>
944
+ ▾ <Suspense>
945
+ ▾ <MaybeSuspend>
946
+ <A key=\\"a\\">
947
+ <Z>
948
+ <Y>",
949
+ "[root]
950
+ ▾ <Root>
951
+ <X>
952
+ ▾ <Suspense>
953
+ ▾ <MaybeSuspend>
954
+ <A key=\\"a\\">
955
+ <Z>
956
+ <Y>",
957
+ "[root]
958
+ ▾ <Root>
959
+ <X>
960
+ ▾ <Suspense>
961
+ ▾ <MaybeSuspend>
962
+ <A key=\\"a\\">
963
+ <B key=\\"b\\">
964
+ <C key=\\"c\\">
965
+ <Z>
966
+ <Y>",
967
+ "[root]
968
+ ▾ <Root>
969
+ <X>
970
+ ▾ <Suspense>
971
+ ▾ <MaybeSuspend>
972
+ <C key=\\"c\\">
973
+ <B key=\\"b\\">
974
+ <A key=\\"a\\">
975
+ <Z>
976
+ <Y>",
977
+ "[root]
978
+ ▾ <Root>
979
+ <X>
980
+ ▾ <Suspense>
981
+ ▾ <MaybeSuspend>
982
+ <C key=\\"c\\">
983
+ <A key=\\"a\\">
984
+ <Z>
985
+ <Y>",
986
+ "[root]
987
+ ▾ <Root>
988
+ <X>
989
+ ▾ <Suspense>
990
+ ▾ <MaybeSuspend>
991
+ <C key=\\"c\\">
992
+ <A key=\\"a\\">
993
+ <Z>
994
+ <Y>",
995
+ "[root]
996
+ ▾ <Root>
997
+ <X>
998
+ ▾ <Suspense>
999
+ ▾ <MaybeSuspend>
1000
+ <C key=\\"c\\">
1001
+ <A key=\\"a\\">
1002
+ <Z>
1003
+ <Y>",
1004
+ "[root]
1005
+ ▾ <Root>
1006
+ <X>
1007
+ ▾ <Suspense>
1008
+ ▾ <MaybeSuspend>
1009
+ <A key=\\"a\\">
1010
+ <B key=\\"b\\">
1011
+ <Z>
1012
+ <Y>",
1013
+ "[root]
1014
+ ▾ <Root>
1015
+ <X>
1016
+ ▾ <Suspense>
1017
+ ▾ <MaybeSuspend>
1018
+ <A key=\\"a\\">
1019
+ <Z>
1020
+ <Y>",
1021
+ "[root]
1022
+ ▾ <Root>
1023
+ <X>
1024
+ ▾ <Suspense>
1025
+ ▾ <MaybeSuspend>
1026
+ <Z>
1027
+ <Y>",
1028
+ "[root]
1029
+ ▾ <Root>
1030
+ <X>
1031
+ ▾ <Suspense>
1032
+ ▾ <MaybeSuspend>
1033
+ <B key=\\"b\\">
1034
+ <Z>
1035
+ <Y>",
1036
+ "[root]
1037
+ ▾ <Root>
1038
+ <X>
1039
+ ▾ <Suspense>
1040
+ ▾ <MaybeSuspend>
1041
+ <A key=\\"a\\">
1042
+ <Z>
1043
+ <Y>",
1044
+ ]
1045
+ `);
1046
+
1047
// 3. Verify we can update from each step to each step in primary mode.
1048
for (let i = 0; i < steps.length; i++) {
1049
for (let j = 0; j < steps.length; j++) {
1050
// Always start with a fresh container and steps[i].
1051
container = document.createElement('div');
1052
// $FlowFixMe
780
- root = ReactDOM.unstable_createRoot(container);
1053
+ root = ReactDOM.createRoot(container);
1054
act(() =>
1055
root.render(
1056
<Root>
@@ -829,7 +1102,7 @@ describe('StoreStressConcurrent', () => {
1102
// Always start with a fresh container and steps[i].
1103
container = document.createElement('div');
1104
// $FlowFixMe
832
- root = ReactDOM.unstable_createRoot(container);
1105
+ root = ReactDOM.createRoot(container);
1106
act(() =>
1107
root.render(
1108
<Root>
@@ -896,7 +1169,7 @@ describe('StoreStressConcurrent', () => {
1169
// Always start with a fresh container and steps[i].
1170
container = document.createElement('div');
1171
// $FlowFixMe
899
- root = ReactDOM.unstable_createRoot(container);
1172
+ root = ReactDOM.createRoot(container);
1173
act(() =>
1174
root.render(
1175
<Root>
@@ -948,7 +1221,7 @@ describe('StoreStressConcurrent', () => {
1221
// Always start with a fresh container and steps[i].
1222
container = document.createElement('div');
1223
// $FlowFixMe
951
- root = ReactDOM.unstable_createRoot(container);
1224
+ root = ReactDOM.createRoot(container);
1225
act(() =>
1226
root.render(
1227
<Root>
@@ -1000,7 +1273,7 @@ describe('StoreStressConcurrent', () => {
1273
// Always start with a fresh container and steps[i].
1274
container = document.createElement('div');
1275
// $FlowFixMe
1003
- root = ReactDOM.unstable_createRoot(container);
1276
+ root = ReactDOM.createRoot(container);
1277
act(() =>
1278
root.render(
1279
<Root>
packages/react-dom/src/__tests__/ReactDOMFiberAsync-test.internal.js
+472
-471
@@ -47,557 +47,558 @@ describe('ReactDOMFiberAsync', () => {
47
expect(ops).toEqual(['Hi', 'Bye']);
48
});
49
50
- it('does not perform deferred updates synchronously', () => {
51
- let inputRef = React.createRef();
52
- let asyncValueRef = React.createRef();
53
- let syncValueRef = React.createRef();
54
-
55
- class Counter extends React.Component {
56
- state = {asyncValue: '', syncValue: ''};
57
-
58
- handleChange = e => {
59
- const nextValue = e.target.value;
60
- requestIdleCallback(() => {
61
- this.setState({
62
- asyncValue: nextValue,
63
- });
64
- // It should not be flushed yet.
65
- expect(asyncValueRef.current.textContent).toBe('');
66
- });
67
- this.setState({
68
- syncValue: nextValue,
69
- });
70
- };
71
-
72
- render() {
73
- return (
74
- <div>
75
- <input
76
- ref={inputRef}
77
- onChange={this.handleChange}
78
- defaultValue=""
79
- />
80
- <p ref={asyncValueRef}>{this.state.asyncValue}</p>
81
- <p ref={syncValueRef}>{this.state.syncValue}</p>
82
- </div>
50
+ if (__EXPERIMENTAL__) {
51
+ describe('concurrent mode', () => {
52
+ beforeEach(() => {
53
+ jest.resetModules();
54
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
55
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
56
+ ReactDOM = require('react-dom');
57
+ Scheduler = require('scheduler');
58
+ });
59
+
60
+ it('does not perform deferred updates synchronously', () => {
61
+ let inputRef = React.createRef();
62
+ let asyncValueRef = React.createRef();
63
+ let syncValueRef = React.createRef();
64
+
65
+ class Counter extends React.Component {
66
+ state = {asyncValue: '', syncValue: ''};
67
+
68
+ handleChange = e => {
69
+ const nextValue = e.target.value;
70
+ requestIdleCallback(() => {
71
+ this.setState({
72
+ asyncValue: nextValue,
73
+ });
74
+ // It should not be flushed yet.
75
+ expect(asyncValueRef.current.textContent).toBe('');
76
+ });
77
+ this.setState({
78
+ syncValue: nextValue,
79
+ });
80
+ };
81
+
82
+ render() {
83
+ return (
84
+ <div>
85
+ <input
86
+ ref={inputRef}
87
+ onChange={this.handleChange}
88
+ defaultValue=""
89
+ />
90
+ <p ref={asyncValueRef}>{this.state.asyncValue}</p>
91
+ <p ref={syncValueRef}>{this.state.syncValue}</p>
92
+ </div>
93
+ );
94
+ }
95
+ }
96
+ const root = ReactDOM.createRoot(container);
97
+ root.render(<Counter />);
98
+ Scheduler.unstable_flushAll();
99
+ expect(asyncValueRef.current.textContent).toBe('');
100
+ expect(syncValueRef.current.textContent).toBe('');
101
+
102
+ setUntrackedInputValue.call(inputRef.current, 'hello');
103
+ inputRef.current.dispatchEvent(
104
+ new MouseEvent('input', {bubbles: true}),
105
);
84
- }
85
- }
86
- const root = ReactDOM.unstable_createRoot(container);
87
- root.render(<Counter />);
88
- Scheduler.unstable_flushAll();
89
- expect(asyncValueRef.current.textContent).toBe('');
90
- expect(syncValueRef.current.textContent).toBe('');
91
-
92
- setUntrackedInputValue.call(inputRef.current, 'hello');
93
- inputRef.current.dispatchEvent(new MouseEvent('input', {bubbles: true}));
94
- // Should only flush non-deferred update.
95
- expect(asyncValueRef.current.textContent).toBe('');
96
- expect(syncValueRef.current.textContent).toBe('hello');
97
-
98
- // Should flush both updates now.
99
- jest.runAllTimers();
100
- Scheduler.unstable_flushAll();
101
- expect(asyncValueRef.current.textContent).toBe('hello');
102
- expect(syncValueRef.current.textContent).toBe('hello');
103
- });
106
+ // Should only flush non-deferred update.
107
+ expect(asyncValueRef.current.textContent).toBe('');
108
+ expect(syncValueRef.current.textContent).toBe('hello');
109
+
110
+ // Should flush both updates now.
111
+ jest.runAllTimers();
112
+ Scheduler.unstable_flushAll();
113
+ expect(asyncValueRef.current.textContent).toBe('hello');
114
+ expect(syncValueRef.current.textContent).toBe('hello');
115
+ });
116
105
- describe('concurrent mode', () => {
106
- beforeEach(() => {
107
- jest.resetModules();
108
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
109
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
110
- ReactDOM = require('react-dom');
111
- Scheduler = require('scheduler');
112
- });
117
+ it('top-level updates are concurrent', () => {
118
+ const root = ReactDOM.createRoot(container);
119
+ root.render(<div>Hi</div>);
120
+ expect(container.textContent).toEqual('');
121
+ Scheduler.unstable_flushAll();
122
+ expect(container.textContent).toEqual('Hi');
123
114
- it('top-level updates are concurrent', () => {
115
- const root = ReactDOM.unstable_createRoot(container);
116
- root.render(<div>Hi</div>);
117
- expect(container.textContent).toEqual('');
118
- Scheduler.unstable_flushAll();
119
- expect(container.textContent).toEqual('Hi');
120
-
121
- root.render(<div>Bye</div>);
122
- expect(container.textContent).toEqual('Hi');
123
- Scheduler.unstable_flushAll();
124
- expect(container.textContent).toEqual('Bye');
125
- });
124
+ root.render(<div>Bye</div>);
125
+ expect(container.textContent).toEqual('Hi');
126
+ Scheduler.unstable_flushAll();
127
+ expect(container.textContent).toEqual('Bye');
128
+ });
129
127
- it('deep updates (setState) are concurrent', () => {
128
- let instance;
129
- class Component extends React.Component {
130
- state = {step: 0};
131
- render() {
132
- instance = this;
133
- return <div>{this.state.step}</div>;
130
+ it('deep updates (setState) are concurrent', () => {
131
+ let instance;
132
+ class Component extends React.Component {
133
+ state = {step: 0};
134
+ render() {
135
+ instance = this;
136
+ return <div>{this.state.step}</div>;
137
+ }
138
}
135
- }
136
-
137
- const root = ReactDOM.unstable_createRoot(container);
138
- root.render(<Component />);
139
- expect(container.textContent).toEqual('');
140
- Scheduler.unstable_flushAll();
141
- expect(container.textContent).toEqual('0');
142
-
143
- instance.setState({step: 1});
144
- expect(container.textContent).toEqual('0');
145
- Scheduler.unstable_flushAll();
146
- expect(container.textContent).toEqual('1');
147
- });
139
149
- it('flushSync batches sync updates and flushes them at the end of the batch', () => {
150
- let ops = [];
151
- let instance;
140
+ const root = ReactDOM.createRoot(container);
141
+ root.render(<Component />);
142
+ expect(container.textContent).toEqual('');
143
+ Scheduler.unstable_flushAll();
144
+ expect(container.textContent).toEqual('0');
145
153
- class Component extends React.Component {
154
- state = {text: ''};
155
- push(val) {
156
- this.setState(state => ({text: state.text + val}));
157
- }
158
- componentDidUpdate() {
159
- ops.push(this.state.text);
160
- }
161
- render() {
162
- instance = this;
163
- return <span>{this.state.text}</span>;
164
- }
165
- }
146
+ instance.setState({step: 1});
147
+ expect(container.textContent).toEqual('0');
148
+ Scheduler.unstable_flushAll();
149
+ expect(container.textContent).toEqual('1');
150
+ });
151
167
- ReactDOM.render(<Component />, container);
152
+ it('flushSync batches sync updates and flushes them at the end of the batch', () => {
153
+ let ops = [];
154
+ let instance;
155
+
156
+ class Component extends React.Component {
157
+ state = {text: ''};
158
+ push(val) {
159
+ this.setState(state => ({text: state.text + val}));
160
+ }
161
+ componentDidUpdate() {
162
+ ops.push(this.state.text);
163
+ }
164
+ render() {
165
+ instance = this;
166
+ return <span>{this.state.text}</span>;
167
+ }
168
+ }
169
169
- instance.push('A');
170
- expect(ops).toEqual(['A']);
171
- expect(container.textContent).toEqual('A');
170
+ ReactDOM.render(<Component />, container);
171
173
- ReactDOM.flushSync(() => {
174
- instance.push('B');
175
- instance.push('C');
176
- // Not flushed yet
177
- expect(container.textContent).toEqual('A');
172
+ instance.push('A');
173
expect(ops).toEqual(['A']);
179
- });
180
- expect(container.textContent).toEqual('ABC');
181
- expect(ops).toEqual(['A', 'ABC']);
182
- instance.push('D');
183
- expect(container.textContent).toEqual('ABCD');
184
- expect(ops).toEqual(['A', 'ABC', 'ABCD']);
185
- });
174
+ expect(container.textContent).toEqual('A');
175
187
- it('flushSync flushes updates even if nested inside another flushSync', () => {
188
- let ops = [];
189
- let instance;
176
+ ReactDOM.flushSync(() => {
177
+ instance.push('B');
178
+ instance.push('C');
179
+ // Not flushed yet
180
+ expect(container.textContent).toEqual('A');
181
+ expect(ops).toEqual(['A']);
182
+ });
183
+ expect(container.textContent).toEqual('ABC');
184
+ expect(ops).toEqual(['A', 'ABC']);
185
+ instance.push('D');
186
+ expect(container.textContent).toEqual('ABCD');
187
+ expect(ops).toEqual(['A', 'ABC', 'ABCD']);
188
+ });
189
191
- class Component extends React.Component {
192
- state = {text: ''};
193
- push(val) {
194
- this.setState(state => ({text: state.text + val}));
190
+ it('flushSync flushes updates even if nested inside another flushSync', () => {
191
+ let ops = [];
192
+ let instance;
193
+
194
+ class Component extends React.Component {
195
+ state = {text: ''};
196
+ push(val) {
197
+ this.setState(state => ({text: state.text + val}));
198
+ }
199
+ componentDidUpdate() {
200
+ ops.push(this.state.text);
201
+ }
202
+ render() {
203
+ instance = this;
204
+ return <span>{this.state.text}</span>;
205
+ }
206
}
196
- componentDidUpdate() {
197
- ops.push(this.state.text);
198
- }
199
- render() {
200
- instance = this;
201
- return <span>{this.state.text}</span>;
202
- }
203
- }
204
-
205
- ReactDOM.render(<Component />, container);
207
207
- instance.push('A');
208
- expect(ops).toEqual(['A']);
209
- expect(container.textContent).toEqual('A');
208
+ ReactDOM.render(<Component />, container);
209
211
- ReactDOM.flushSync(() => {
212
- instance.push('B');
213
- instance.push('C');
214
- // Not flushed yet
215
- expect(container.textContent).toEqual('A');
210
+ instance.push('A');
211
expect(ops).toEqual(['A']);
212
+ expect(container.textContent).toEqual('A');
213
214
ReactDOM.flushSync(() => {
219
- instance.push('D');
215
+ instance.push('B');
216
+ instance.push('C');
217
+ // Not flushed yet
218
+ expect(container.textContent).toEqual('A');
219
+ expect(ops).toEqual(['A']);
220
+
221
+ ReactDOM.flushSync(() => {
222
+ instance.push('D');
223
+ });
224
+ // The nested flushSync caused everything to flush.
225
+ expect(container.textContent).toEqual('ABCD');
226
+ expect(ops).toEqual(['A', 'ABCD']);
227
});
221
- // The nested flushSync caused everything to flush.
228
expect(container.textContent).toEqual('ABCD');
229
expect(ops).toEqual(['A', 'ABCD']);
230
});
225
- expect(container.textContent).toEqual('ABCD');
226
- expect(ops).toEqual(['A', 'ABCD']);
227
- });
231
229
- it('flushSync throws if already performing work', () => {
230
- class Component extends React.Component {
231
- componentDidUpdate() {
232
- ReactDOM.flushSync(() => {});
233
- }
234
- render() {
235
- return null;
232
+ it('flushSync throws if already performing work', () => {
233
+ class Component extends React.Component {
234
+ componentDidUpdate() {
235
+ ReactDOM.flushSync(() => {});
236
+ }
237
+ render() {
238
+ return null;
239
+ }
240
}
237
- }
238
-
239
- // Initial mount
240
- ReactDOM.render(<Component />, container);
241
- // Update
242
- expect(() => ReactDOM.render(<Component />, container)).toThrow(
243
- 'flushSync was called from inside a lifecycle method',
244
- );
245
- });
241
247
- it('flushSync flushes updates before end of the tick', () => {
248
- let ops = [];
249
- let instance;
242
+ // Initial mount
243
+ ReactDOM.render(<Component />, container);
244
+ // Update
245
+ expect(() => ReactDOM.render(<Component />, container)).toThrow(
246
+ 'flushSync was called from inside a lifecycle method',
247
+ );
248
+ });
249
251
- class Component extends React.Component {
252
- state = {text: ''};
253
- push(val) {
254
- this.setState(state => ({text: state.text + val}));
255
- }
256
- componentDidUpdate() {
257
- ops.push(this.state.text);
258
- }
259
- render() {
260
- instance = this;
261
- return <span>{this.state.text}</span>;
250
+ it('flushSync flushes updates before end of the tick', () => {
251
+ let ops = [];
252
+ let instance;
253
+
254
+ class Component extends React.Component {
255
+ state = {text: ''};
256
+ push(val) {
257
+ this.setState(state => ({text: state.text + val}));
258
+ }
259
+ componentDidUpdate() {
260
+ ops.push(this.state.text);
261
+ }
262
+ render() {
263
+ instance = this;
264
+ return <span>{this.state.text}</span>;
265
+ }
266
}
263
- }
267
265
- const root = ReactDOM.unstable_createRoot(container);
266
- root.render(<Component />);
267
- Scheduler.unstable_flushAll();
268
+ const root = ReactDOM.createRoot(container);
269
+ root.render(<Component />);
270
+ Scheduler.unstable_flushAll();
271
269
- // Updates are async by default
270
- instance.push('A');
271
- expect(ops).toEqual([]);
272
- expect(container.textContent).toEqual('');
273
-
274
- ReactDOM.flushSync(() => {
275
- instance.push('B');
276
- instance.push('C');
277
- // Not flushed yet
278
- expect(container.textContent).toEqual('');
272
+ // Updates are async by default
273
+ instance.push('A');
274
expect(ops).toEqual([]);
275
+ expect(container.textContent).toEqual('');
276
+
277
+ ReactDOM.flushSync(() => {
278
+ instance.push('B');
279
+ instance.push('C');
280
+ // Not flushed yet
281
+ expect(container.textContent).toEqual('');
282
+ expect(ops).toEqual([]);
283
+ });
284
+ // Only the active updates have flushed
285
+ expect(container.textContent).toEqual('BC');
286
+ expect(ops).toEqual(['BC']);
287
+
288
+ instance.push('D');
289
+ expect(container.textContent).toEqual('BC');
290
+ expect(ops).toEqual(['BC']);
291
+
292
+ // Flush the async updates
293
+ Scheduler.unstable_flushAll();
294
+ expect(container.textContent).toEqual('ABCD');
295
+ expect(ops).toEqual(['BC', 'ABCD']);
296
});
281
- // Only the active updates have flushed
282
- expect(container.textContent).toEqual('BC');
283
- expect(ops).toEqual(['BC']);
284
-
285
- instance.push('D');
286
- expect(container.textContent).toEqual('BC');
287
- expect(ops).toEqual(['BC']);
288
-
289
- // Flush the async updates
290
- Scheduler.unstable_flushAll();
291
- expect(container.textContent).toEqual('ABCD');
292
- expect(ops).toEqual(['BC', 'ABCD']);
293
- });
297
295
- it('flushControlled flushes updates before yielding to browser', () => {
296
- let inst;
297
- class Counter extends React.Component {
298
- state = {counter: 0};
299
- increment = () =>
300
- this.setState(state => ({counter: state.counter + 1}));
301
- render() {
302
- inst = this;
303
- return this.state.counter;
298
+ it('flushControlled flushes updates before yielding to browser', () => {
299
+ let inst;
300
+ class Counter extends React.Component {
301
+ state = {counter: 0};
302
+ increment = () =>
303
+ this.setState(state => ({counter: state.counter + 1}));
304
+ render() {
305
+ inst = this;
306
+ return this.state.counter;
307
+ }
308
}
305
- }
306
- const root = ReactDOM.unstable_createRoot(container);
307
- root.render(<Counter />);
308
- Scheduler.unstable_flushAll();
309
- expect(container.textContent).toEqual('0');
310
-
311
- // Test that a normal update is async
312
- inst.increment();
313
- expect(container.textContent).toEqual('0');
314
- Scheduler.unstable_flushAll();
315
- expect(container.textContent).toEqual('1');
316
-
317
- let ops = [];
318
- ReactDOM.unstable_flushControlled(() => {
309
+ const root = ReactDOM.createRoot(container);
310
+ root.render(<Counter />);
311
+ Scheduler.unstable_flushAll();
312
+ expect(container.textContent).toEqual('0');
313
+
314
+ // Test that a normal update is async
315
inst.increment();
316
+ expect(container.textContent).toEqual('0');
317
+ Scheduler.unstable_flushAll();
318
+ expect(container.textContent).toEqual('1');
319
+
320
+ let ops = [];
321
ReactDOM.unstable_flushControlled(() => {
322
inst.increment();
322
- ops.push('end of inner flush: ' + container.textContent);
323
+ ReactDOM.unstable_flushControlled(() => {
324
+ inst.increment();
325
+ ops.push('end of inner flush: ' + container.textContent);
326
+ });
327
+ ops.push('end of outer flush: ' + container.textContent);
328
});
324
- ops.push('end of outer flush: ' + container.textContent);
329
+ ops.push('after outer flush: ' + container.textContent);
330
+ expect(ops).toEqual([
331
+ 'end of inner flush: 1',
332
+ 'end of outer flush: 1',
333
+ 'after outer flush: 3',
334
+ ]);
335
});
326
- ops.push('after outer flush: ' + container.textContent);
327
- expect(ops).toEqual([
328
- 'end of inner flush: 1',
329
- 'end of outer flush: 1',
330
- 'after outer flush: 3',
331
- ]);
332
- });
336
334
- it('flushControlled does not flush until end of outermost batchedUpdates', () => {
335
- let inst;
336
- class Counter extends React.Component {
337
- state = {counter: 0};
338
- increment = () =>
339
- this.setState(state => ({counter: state.counter + 1}));
340
- render() {
341
- inst = this;
342
- return this.state.counter;
337
+ it('flushControlled does not flush until end of outermost batchedUpdates', () => {
338
+ let inst;
339
+ class Counter extends React.Component {
340
+ state = {counter: 0};
341
+ increment = () =>
342
+ this.setState(state => ({counter: state.counter + 1}));
343
+ render() {
344
+ inst = this;
345
+ return this.state.counter;
346
+ }
347
}
344
- }
345
- ReactDOM.render(<Counter />, container);
348
+ ReactDOM.render(<Counter />, container);
349
347
- let ops = [];
348
- ReactDOM.unstable_batchedUpdates(() => {
349
- inst.increment();
350
- ReactDOM.unstable_flushControlled(() => {
350
+ let ops = [];
351
+ ReactDOM.unstable_batchedUpdates(() => {
352
inst.increment();
352
- ops.push('end of flushControlled fn: ' + container.textContent);
353
+ ReactDOM.unstable_flushControlled(() => {
354
+ inst.increment();
355
+ ops.push('end of flushControlled fn: ' + container.textContent);
356
+ });
357
+ ops.push('end of batchedUpdates fn: ' + container.textContent);
358
});
354
- ops.push('end of batchedUpdates fn: ' + container.textContent);
359
+ ops.push('after batchedUpdates: ' + container.textContent);
360
+ expect(ops).toEqual([
361
+ 'end of flushControlled fn: 0',
362
+ 'end of batchedUpdates fn: 0',
363
+ 'after batchedUpdates: 2',
364
+ ]);
365
});
356
- ops.push('after batchedUpdates: ' + container.textContent);
357
- expect(ops).toEqual([
358
- 'end of flushControlled fn: 0',
359
- 'end of batchedUpdates fn: 0',
360
- 'after batchedUpdates: 2',
361
- ]);
362
- });
366
364
- it('flushControlled returns nothing', () => {
365
- // In the future, we may want to return a thenable "work" object.
366
- let inst;
367
- class Counter extends React.Component {
368
- state = {counter: 0};
369
- increment = () =>
370
- this.setState(state => ({counter: state.counter + 1}));
371
- render() {
372
- inst = this;
373
- return this.state.counter;
367
+ it('flushControlled returns nothing', () => {
368
+ // In the future, we may want to return a thenable "work" object.
369
+ let inst;
370
+ class Counter extends React.Component {
371
+ state = {counter: 0};
372
+ increment = () =>
373
+ this.setState(state => ({counter: state.counter + 1}));
374
+ render() {
375
+ inst = this;
376
+ return this.state.counter;
377
+ }
378
}
375
- }
376
- ReactDOM.render(<Counter />, container);
377
- expect(container.textContent).toEqual('0');
379
+ ReactDOM.render(<Counter />, container);
380
+ expect(container.textContent).toEqual('0');
381
379
- const returnValue = ReactDOM.unstable_flushControlled(() => {
380
- inst.increment();
381
- return 'something';
382
+ const returnValue = ReactDOM.unstable_flushControlled(() => {
383
+ inst.increment();
384
+ return 'something';
385
+ });
386
+ expect(container.textContent).toEqual('1');
387
+ expect(returnValue).toBe(undefined);
388
});
383
- expect(container.textContent).toEqual('1');
384
- expect(returnValue).toBe(undefined);
385
- });
389
387
- it('ignores discrete events on a pending removed element', () => {
388
- const disableButtonRef = React.createRef();
389
- const submitButtonRef = React.createRef();
390
-
391
- let formSubmitted = false;
392
-
393
- class Form extends React.Component {
394
- state = {active: true};
395
- disableForm = () => {
396
- this.setState({active: false});
397
- };
398
- submitForm = () => {
399
- formSubmitted = true; // This should not get invoked
400
- };
401
- render() {
402
- return (
403
- <div>
404
- <button onClick={this.disableForm} ref={disableButtonRef}>
405
- Disable
406
- </button>
407
- {this.state.active ? (
408
- <button onClick={this.submitForm} ref={submitButtonRef}>
409
- Submit
390
+ it('ignores discrete events on a pending removed element', () => {
391
+ const disableButtonRef = React.createRef();
392
+ const submitButtonRef = React.createRef();
393
+
394
+ let formSubmitted = false;
395
+
396
+ class Form extends React.Component {
397
+ state = {active: true};
398
+ disableForm = () => {
399
+ this.setState({active: false});
400
+ };
401
+ submitForm = () => {
402
+ formSubmitted = true; // This should not get invoked
403
+ };
404
+ render() {
405
+ return (
406
+ <div>
407
+ <button onClick={this.disableForm} ref={disableButtonRef}>
408
+ Disable
409
</button>
411
- ) : null}
412
- </div>
413
- );
410
+ {this.state.active ? (
411
+ <button onClick={this.submitForm} ref={submitButtonRef}>
412
+ Submit
413
+ </button>
414
+ ) : null}
415
+ </div>
416
+ );
417
+ }
418
}
415
- }
419
417
- const root = ReactDOM.unstable_createRoot(container);
418
- root.render(<Form />);
419
- // Flush
420
- Scheduler.unstable_flushAll();
420
+ const root = ReactDOM.createRoot(container);
421
+ root.render(<Form />);
422
+ // Flush
423
+ Scheduler.unstable_flushAll();
424
422
- let disableButton = disableButtonRef.current;
423
- expect(disableButton.tagName).toBe('BUTTON');
425
+ let disableButton = disableButtonRef.current;
426
+ expect(disableButton.tagName).toBe('BUTTON');
427
425
- // Dispatch a click event on the Disable-button.
426
- let firstEvent = document.createEvent('Event');
427
- firstEvent.initEvent('click', true, true);
428
- disableButton.dispatchEvent(firstEvent);
428
+ // Dispatch a click event on the Disable-button.
429
+ let firstEvent = document.createEvent('Event');
430
+ firstEvent.initEvent('click', true, true);
431
+ disableButton.dispatchEvent(firstEvent);
432
430
- // There should now be a pending update to disable the form.
433
+ // There should now be a pending update to disable the form.
434
432
- // This should not have flushed yet since it's in concurrent mode.
433
- let submitButton = submitButtonRef.current;
434
- expect(submitButton.tagName).toBe('BUTTON');
435
+ // This should not have flushed yet since it's in concurrent mode.
436
+ let submitButton = submitButtonRef.current;
437
+ expect(submitButton.tagName).toBe('BUTTON');
438
436
- // In the meantime, we can dispatch a new client event on the submit button.
437
- let secondEvent = document.createEvent('Event');
438
- secondEvent.initEvent('click', true, true);
439
- // This should force the pending update to flush which disables the submit button before the event is invoked.
440
- submitButton.dispatchEvent(secondEvent);
439
+ // In the meantime, we can dispatch a new client event on the submit button.
440
+ let secondEvent = document.createEvent('Event');
441
+ secondEvent.initEvent('click', true, true);
442
+ // This should force the pending update to flush which disables the submit button before the event is invoked.
443
+ submitButton.dispatchEvent(secondEvent);
444
442
- // Therefore the form should never have been submitted.
443
- expect(formSubmitted).toBe(false);
445
+ // Therefore the form should never have been submitted.
446
+ expect(formSubmitted).toBe(false);
447
445
- expect(submitButtonRef.current).toBe(null);
446
- });
448
+ expect(submitButtonRef.current).toBe(null);
449
+ });
450
448
- it('ignores discrete events on a pending removed event listener', () => {
449
- const disableButtonRef = React.createRef();
450
- const submitButtonRef = React.createRef();
451
-
452
- let formSubmitted = false;
453
-
454
- class Form extends React.Component {
455
- state = {active: true};
456
- disableForm = () => {
457
- this.setState({active: false});
458
- };
459
- submitForm = () => {
460
- formSubmitted = true; // This should not get invoked
461
- };
462
- disabledSubmitForm = () => {
463
- // The form is disabled.
464
- };
465
- render() {
466
- return (
467
- <div>
468
- <button onClick={this.disableForm} ref={disableButtonRef}>
469
- Disable
470
- </button>
471
- <button
472
- onClick={
473
- this.state.active ? this.submitForm : this.disabledSubmitForm
474
- }
475
- ref={submitButtonRef}>
476
- Submit
477
- </button>
478
- </div>
479
- );
451
+ it('ignores discrete events on a pending removed event listener', () => {
452
+ const disableButtonRef = React.createRef();
453
+ const submitButtonRef = React.createRef();
454
+
455
+ let formSubmitted = false;
456
+
457
+ class Form extends React.Component {
458
+ state = {active: true};
459
+ disableForm = () => {
460
+ this.setState({active: false});
461
+ };
462
+ submitForm = () => {
463
+ formSubmitted = true; // This should not get invoked
464
+ };
465
+ disabledSubmitForm = () => {
466
+ // The form is disabled.
467
+ };
468
+ render() {
469
+ return (
470
+ <div>
471
+ <button onClick={this.disableForm} ref={disableButtonRef}>
472
+ Disable
473
+ </button>
474
+ <button
475
+ onClick={
476
+ this.state.active
477
+ ? this.submitForm
478
+ : this.disabledSubmitForm
479
+ }
480
+ ref={submitButtonRef}>
481
+ Submit
482
+ </button>
483
+ </div>
484
+ );
485
+ }
486
}
481
- }
487
483
- const root = ReactDOM.unstable_createRoot(container);
484
- root.render(<Form />);
485
- // Flush
486
- Scheduler.unstable_flushAll();
488
+ const root = ReactDOM.createRoot(container);
489
+ root.render(<Form />);
490
+ // Flush
491
+ Scheduler.unstable_flushAll();
492
488
- let disableButton = disableButtonRef.current;
489
- expect(disableButton.tagName).toBe('BUTTON');
493
+ let disableButton = disableButtonRef.current;
494
+ expect(disableButton.tagName).toBe('BUTTON');
495
491
- // Dispatch a click event on the Disable-button.
492
- let firstEvent = document.createEvent('Event');
493
- firstEvent.initEvent('click', true, true);
494
- disableButton.dispatchEvent(firstEvent);
496
+ // Dispatch a click event on the Disable-button.
497
+ let firstEvent = document.createEvent('Event');
498
+ firstEvent.initEvent('click', true, true);
499
+ disableButton.dispatchEvent(firstEvent);
500
496
- // There should now be a pending update to disable the form.
501
+ // There should now be a pending update to disable the form.
502
498
- // This should not have flushed yet since it's in concurrent mode.
499
- let submitButton = submitButtonRef.current;
500
- expect(submitButton.tagName).toBe('BUTTON');
503
+ // This should not have flushed yet since it's in concurrent mode.
504
+ let submitButton = submitButtonRef.current;
505
+ expect(submitButton.tagName).toBe('BUTTON');
506
502
- // In the meantime, we can dispatch a new client event on the submit button.
503
- let secondEvent = document.createEvent('Event');
504
- secondEvent.initEvent('click', true, true);
505
- // This should force the pending update to flush which disables the submit button before the event is invoked.
506
- submitButton.dispatchEvent(secondEvent);
507
+ // In the meantime, we can dispatch a new client event on the submit button.
508
+ let secondEvent = document.createEvent('Event');
509
+ secondEvent.initEvent('click', true, true);
510
+ // This should force the pending update to flush which disables the submit button before the event is invoked.
511
+ submitButton.dispatchEvent(secondEvent);
512
508
- // Therefore the form should never have been submitted.
509
- expect(formSubmitted).toBe(false);
510
- });
513
+ // Therefore the form should never have been submitted.
514
+ expect(formSubmitted).toBe(false);
515
+ });
516
512
- it('uses the newest discrete events on a pending changed event listener', () => {
513
- const enableButtonRef = React.createRef();
514
- const submitButtonRef = React.createRef();
515
-
516
- let formSubmitted = false;
517
-
518
- class Form extends React.Component {
519
- state = {active: false};
520
- enableForm = () => {
521
- this.setState({active: true});
522
- };
523
- submitForm = () => {
524
- formSubmitted = true; // This should happen
525
- };
526
- render() {
527
- return (
528
- <div>
529
- <button onClick={this.enableForm} ref={enableButtonRef}>
530
- Enable
531
- </button>
532
- <button
533
- onClick={this.state.active ? this.submitForm : null}
534
- ref={submitButtonRef}>
535
- Submit
536
- </button>{' '}
537
- : null}
538
- </div>
539
- );
517
+ it('uses the newest discrete events on a pending changed event listener', () => {
518
+ const enableButtonRef = React.createRef();
519
+ const submitButtonRef = React.createRef();
520
+
521
+ let formSubmitted = false;
522
+
523
+ class Form extends React.Component {
524
+ state = {active: false};
525
+ enableForm = () => {
526
+ this.setState({active: true});
527
+ };
528
+ submitForm = () => {
529
+ formSubmitted = true; // This should happen
530
+ };
531
+ render() {
532
+ return (
533
+ <div>
534
+ <button onClick={this.enableForm} ref={enableButtonRef}>
535
+ Enable
536
+ </button>
537
+ <button
538
+ onClick={this.state.active ? this.submitForm : null}
539
+ ref={submitButtonRef}>
540
+ Submit
541
+ </button>{' '}
542
+ : null}
543
+ </div>
544
+ );
545
+ }
546
}
541
- }
547
543
- const root = ReactDOM.unstable_createRoot(container);
544
- root.render(<Form />);
545
- // Flush
546
- Scheduler.unstable_flushAll();
548
+ const root = ReactDOM.createRoot(container);
549
+ root.render(<Form />);
550
+ // Flush
551
+ Scheduler.unstable_flushAll();
552
548
- let enableButton = enableButtonRef.current;
549
- expect(enableButton.tagName).toBe('BUTTON');
553
+ let enableButton = enableButtonRef.current;
554
+ expect(enableButton.tagName).toBe('BUTTON');
555
551
- // Dispatch a click event on the Enable-button.
552
- let firstEvent = document.createEvent('Event');
553
- firstEvent.initEvent('click', true, true);
554
- enableButton.dispatchEvent(firstEvent);
556
+ // Dispatch a click event on the Enable-button.
557
+ let firstEvent = document.createEvent('Event');
558
+ firstEvent.initEvent('click', true, true);
559
+ enableButton.dispatchEvent(firstEvent);
560
556
- // There should now be a pending update to enable the form.
561
+ // There should now be a pending update to enable the form.
562
558
- // This should not have flushed yet since it's in concurrent mode.
559
- let submitButton = submitButtonRef.current;
560
- expect(submitButton.tagName).toBe('BUTTON');
563
+ // This should not have flushed yet since it's in concurrent mode.
564
+ let submitButton = submitButtonRef.current;
565
+ expect(submitButton.tagName).toBe('BUTTON');
566
562
- // In the meantime, we can dispatch a new client event on the submit button.
563
- let secondEvent = document.createEvent('Event');
564
- secondEvent.initEvent('click', true, true);
565
- // This should force the pending update to flush which enables the submit button before the event is invoked.
566
- submitButton.dispatchEvent(secondEvent);
567
+ // In the meantime, we can dispatch a new client event on the submit button.
568
+ let secondEvent = document.createEvent('Event');
569
+ secondEvent.initEvent('click', true, true);
570
+ // This should force the pending update to flush which enables the submit button before the event is invoked.
571
+ submitButton.dispatchEvent(secondEvent);
572
568
- // Therefore the form should have been submitted.
569
- expect(formSubmitted).toBe(true);
573
+ // Therefore the form should have been submitted.
574
+ expect(formSubmitted).toBe(true);
575
+ });
576
});
571
- });
577
573
- describe('createSyncRoot', () => {
574
- it('updates flush without yielding in the next event', () => {
575
- const root = ReactDOM.unstable_createSyncRoot(container);
576
-
577
- function Text(props) {
578
- Scheduler.unstable_yieldValue(props.text);
579
- return props.text;
580
- }
581
-
582
- root.render(
583
- <>
584
- <Text text="A" />
585
- <Text text="B" />
586
- <Text text="C" />
587
- </>,
588
- );
589
-
590
- // Nothing should have rendered yet
591
- expect(container.textContent).toEqual('');
592
-
593
- // Everything should render immediately in the next event
594
- expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
595
- expect(container.textContent).toEqual('ABC');
596
- });
578
+ describe('createSyncRoot', () => {
579
+ it('updates flush without yielding in the next event', () => {
580
+ const root = ReactDOM.createSyncRoot(container);
581
+
582
+ function Text(props) {
583
+ Scheduler.unstable_yieldValue(props.text);
584
+ return props.text;
585
+ }
586
+
587
+ root.render(
588
+ <>
589
+ <Text text="A" />
590
+ <Text text="B" />
591
+ <Text text="C" />
592
+ </>,
593
+ );
594
+
595
+ // Nothing should have rendered yet
596
+ expect(container.textContent).toEqual('');
597
598
- it('does not support createBatch', () => {
599
- const root = ReactDOM.unstable_createSyncRoot(container);
600
- expect(root.createBatch).toBe(undefined);
598
+ // Everything should render immediately in the next event
599
+ expect(Scheduler).toFlushExpired(['A', 'B', 'C']);
600
+ expect(container.textContent).toEqual('ABC');
601
+ });
602
});
602
- });
603
+ }
604
});
packages/react-dom/src/__tests__/ReactDOMHooks-test.js
+33
-31
@@ -105,38 +105,40 @@ describe('ReactDOMHooks', () => {
105
expect(labelRef.current.innerHTML).toBe('abc');
106
});
107
108
- it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => {
109
- const {createRef, useCallback, useState} = React;
110
-
111
- const Example = ({inputRef, labelRef}) => {
112
- const [text, setText] = useState('');
113
- const handleInput = useCallback(event => {
114
- setText(event.target.value);
115
- });
116
-
117
- return (
118
- <>
119
- <input ref={inputRef} onInput={handleInput} />
120
- <label ref={labelRef}>{text}</label>
121
- </>
108
+ if (__EXPERIMENTAL__) {
109
+ it('should not bail out when an update is scheduled from within an event handler in Concurrent Mode', () => {
110
+ const {createRef, useCallback, useState} = React;
111
+
112
+ const Example = ({inputRef, labelRef}) => {
113
+ const [text, setText] = useState('');
114
+ const handleInput = useCallback(event => {
115
+ setText(event.target.value);
116
+ });
117
+
118
+ return (
119
+ <>
120
+ <input ref={inputRef} onInput={handleInput} />
121
+ <label ref={labelRef}>{text}</label>
122
+ </>
123
+ );
124
+ };
125
+
126
+ const inputRef = createRef();
127
+ const labelRef = createRef();
128
+
129
+ const root = ReactDOM.createRoot(container);
130
+ root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
131
+
132
+ Scheduler.unstable_flushAll();
133
+
134
+ inputRef.current.value = 'abc';
135
+ inputRef.current.dispatchEvent(
136
+ new Event('input', {bubbles: true, cancelable: true}),
137
);
123
- };
138
125
- const inputRef = createRef();
126
- const labelRef = createRef();
127
-
128
- const root = ReactDOM.unstable_createRoot(container);
129
- root.render(<Example inputRef={inputRef} labelRef={labelRef} />);
130
-
131
- Scheduler.unstable_flushAll();
132
-
133
- inputRef.current.value = 'abc';
134
- inputRef.current.dispatchEvent(
135
- new Event('input', {bubbles: true, cancelable: true}),
136
- );
139
+ Scheduler.unstable_flushAll();
140
138
- Scheduler.unstable_flushAll();
139
-
140
- expect(labelRef.current.innerHTML).toBe('abc');
141
- });
141
+ expect(labelRef.current.innerHTML).toBe('abc');
142
+ });
143
+ }
144
});
packages/react-dom/src/__tests__/ReactDOMRoot-test.js
+18
-11
@@ -26,15 +26,22 @@ describe('ReactDOMRoot', () => {
26
Scheduler = require('scheduler');
27
});
28
29
+ if (!__EXPERIMENTAL__) {
30
+ it('createRoot is not exposed in stable build', () => {
31
+ expect(ReactDOM.createRoot).toBe(undefined);
32
+ });
33
+ return;
34
+ }
35
+
36
it('renders children', () => {
30
- const root = ReactDOM.unstable_createRoot(container);
37
+ const root = ReactDOM.createRoot(container);
38
root.render(<div>Hi</div>);
39
Scheduler.unstable_flushAll();
40
expect(container.textContent).toEqual('Hi');
41
});
42
43
it('unmounts children', () => {
37
- const root = ReactDOM.unstable_createRoot(container);
44
+ const root = ReactDOM.createRoot(container);
45
root.render(<div>Hi</div>);
46
Scheduler.unstable_flushAll();
47
expect(container.textContent).toEqual('Hi');
@@ -57,7 +64,7 @@ describe('ReactDOMRoot', () => {
64
// Does not hydrate by default
65
const container1 = document.createElement('div');
66
container1.innerHTML = markup;
60
- const root1 = ReactDOM.unstable_createRoot(container1);
67
+ const root1 = ReactDOM.createRoot(container1);
68
root1.render(
69
<div>
70
<span />
@@ -68,7 +75,7 @@ describe('ReactDOMRoot', () => {
75
// Accepts `hydrate` option
76
const container2 = document.createElement('div');
77
container2.innerHTML = markup;
71
- const root2 = ReactDOM.unstable_createRoot(container2, {hydrate: true});
78
+ const root2 = ReactDOM.createRoot(container2, {hydrate: true});
79
root2.render(
80
<div>
81
<span />
@@ -81,7 +88,7 @@ describe('ReactDOMRoot', () => {
88
89
it('does not clear existing children', async () => {
90
container.innerHTML = '<div>a</div><div>b</div>';
84
- const root = ReactDOM.unstable_createRoot(container);
91
+ const root = ReactDOM.createRoot(container);
92
root.render(
93
<div>
94
<span>c</span>
@@ -102,12 +109,12 @@ describe('ReactDOMRoot', () => {
109
110
it('throws a good message on invalid containers', () => {
111
expect(() => {
105
- ReactDOM.unstable_createRoot(<div>Hi</div>);
112
+ ReactDOM.createRoot(<div>Hi</div>);
113
}).toThrow('createRoot(...): Target container is not a DOM element.');
114
});
115
116
it('warns when rendering with legacy API into createRoot() container', () => {
110
- const root = ReactDOM.unstable_createRoot(container);
117
+ const root = ReactDOM.createRoot(container);
118
root.render(<div>Hi</div>);
119
Scheduler.unstable_flushAll();
120
expect(container.textContent).toEqual('Hi');
@@ -130,7 +137,7 @@ describe('ReactDOMRoot', () => {
137
});
138
139
it('warns when hydrating with legacy API into createRoot() container', () => {
133
- const root = ReactDOM.unstable_createRoot(container);
140
+ const root = ReactDOM.createRoot(container);
141
root.render(<div>Hi</div>);
142
Scheduler.unstable_flushAll();
143
expect(container.textContent).toEqual('Hi');
@@ -150,7 +157,7 @@ describe('ReactDOMRoot', () => {
157
});
158
159
it('warns when unmounting with legacy API (no previous content)', () => {
153
- const root = ReactDOM.unstable_createRoot(container);
160
+ const root = ReactDOM.createRoot(container);
161
root.render(<div>Hi</div>);
162
Scheduler.unstable_flushAll();
163
expect(container.textContent).toEqual('Hi');
@@ -179,7 +186,7 @@ describe('ReactDOMRoot', () => {
186
// Currently createRoot().render() doesn't clear this.
187
container.appendChild(document.createElement('div'));
188
// The rest is the same as test above.
182
- const root = ReactDOM.unstable_createRoot(container);
189
+ const root = ReactDOM.createRoot(container);
190
root.render(<div>Hi</div>);
191
Scheduler.unstable_flushAll();
192
expect(container.textContent).toEqual('Hi');
@@ -198,7 +205,7 @@ describe('ReactDOMRoot', () => {
205
it('warns when passing legacy container to createRoot()', () => {
206
ReactDOM.render(<div>Hi</div>, container);
207
expect(() => {
201
- ReactDOM.unstable_createRoot(container);
208
+ ReactDOM.createRoot(container);
209
}).toWarnDev(
210
'You are calling ReactDOM.createRoot() on a container that was previously ' +
211
'passed to ReactDOM.render(). This is not supported.',
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+37
-33
@@ -75,7 +75,6 @@ describe('ReactDOMServerPartialHydration', () => {
75
jest.resetModuleRegistry();
76
77
ReactFeatureFlags = require('shared/ReactFeatureFlags');
78
- ReactFeatureFlags.enableSuspenseServerRenderer = true;
78
ReactFeatureFlags.enableSuspenseCallback = true;
79
ReactFeatureFlags.enableFlareAPI = true;
80
@@ -90,6 +89,11 @@ describe('ReactDOMServerPartialHydration', () => {
89
useHover = require('react-interactions/events/hover').useHover;
90
});
91
92
+ if (!__EXPERIMENTAL__) {
93
+ it("empty test so Jest doesn't complain", () => {});
94
+ return;
95
+ }
96
+
97
it('hydrates a parent even if a child Suspense boundary is blocked', async () => {
98
let suspend = false;
99
let resolve;
@@ -130,7 +134,7 @@ describe('ReactDOMServerPartialHydration', () => {
134
// On the client we don't have all data yet but we want to start
135
// hydrating anyway.
136
suspend = true;
133
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
137
+ let root = ReactDOM.createRoot(container, {hydrate: true});
138
root.render(<App />);
139
Scheduler.unstable_flushAll();
140
jest.runAllTimers();
@@ -200,7 +204,7 @@ describe('ReactDOMServerPartialHydration', () => {
204
// hydrating anyway.
205
suspend = true;
206
suspend2 = true;
203
- let root = ReactDOM.unstable_createRoot(container, {
207
+ let root = ReactDOM.createRoot(container, {
208
hydrate: true,
209
hydrationOptions: {
210
onHydrated(node) {
@@ -273,7 +277,7 @@ describe('ReactDOMServerPartialHydration', () => {
277
// On the client we don't have all data yet but we want to start
278
// hydrating anyway.
279
suspend = true;
276
- let root = ReactDOM.unstable_createRoot(container, {
280
+ let root = ReactDOM.createRoot(container, {
281
hydrate: true,
282
hydrationOptions: {
283
onDeleted(node) {
@@ -411,7 +415,7 @@ describe('ReactDOMServerPartialHydration', () => {
415
suspend = true;
416
417
act(() => {
414
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
418
+ let root = ReactDOM.createRoot(container, {hydrate: true});
419
root.render(<App />);
420
});
421
@@ -468,7 +472,7 @@ describe('ReactDOMServerPartialHydration', () => {
472
// hydrating anyway.
473
suspend = true;
474
act(() => {
471
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
475
+ let root = ReactDOM.createRoot(container, {hydrate: true});
476
root.render(<App />);
477
});
478
@@ -518,7 +522,7 @@ describe('ReactDOMServerPartialHydration', () => {
522
// On the client we don't have all data yet but we want to start
523
// hydrating anyway.
524
suspend = true;
521
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
525
+ let root = ReactDOM.createRoot(container, {hydrate: true});
526
root.render(<App text="Hello" className="hello" />);
527
Scheduler.unstable_flushAll();
528
jest.runAllTimers();
@@ -587,7 +591,7 @@ describe('ReactDOMServerPartialHydration', () => {
591
// On the client we don't have all data yet but we want to start
592
// hydrating anyway.
593
suspend = true;
590
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
594
+ let root = ReactDOM.createRoot(container, {hydrate: true});
595
root.render(<App text="Hello" className="hello" />);
596
Scheduler.unstable_flushAll();
597
jest.runAllTimers();
@@ -660,7 +664,7 @@ describe('ReactDOMServerPartialHydration', () => {
664
// On the client we don't have all data yet but we want to start
665
// hydrating anyway.
666
suspend = true;
663
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
667
+ let root = ReactDOM.createRoot(container, {hydrate: true});
668
root.render(<App text="Hello" className="hello" />);
669
Scheduler.unstable_flushAll();
670
jest.runAllTimers();
@@ -732,7 +736,7 @@ describe('ReactDOMServerPartialHydration', () => {
736
// On the client we don't have all data yet but we want to start
737
// hydrating anyway.
738
suspend = true;
735
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
739
+ let root = ReactDOM.createRoot(container, {hydrate: true});
740
root.render(<App text="Hello" className="hello" />);
741
Scheduler.unstable_flushAll();
742
jest.runAllTimers();
@@ -803,7 +807,7 @@ describe('ReactDOMServerPartialHydration', () => {
807
// On the client we don't have all data yet but we want to start
808
// hydrating anyway.
809
suspend = true;
806
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
810
+ let root = ReactDOM.createRoot(container, {hydrate: true});
811
root.render(<App text="Hello" className="hello" />);
812
Scheduler.unstable_flushAll();
813
jest.runAllTimers();
@@ -889,7 +893,7 @@ describe('ReactDOMServerPartialHydration', () => {
893
// On the client we don't have all data yet but we want to start
894
// hydrating anyway.
895
suspend = true;
892
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
896
+ let root = ReactDOM.createRoot(container, {hydrate: true});
897
root.render(
898
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
899
<App />
@@ -971,7 +975,7 @@ describe('ReactDOMServerPartialHydration', () => {
975
// On the client we don't have all data yet but we want to start
976
// hydrating anyway.
977
suspend = true;
974
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
978
+ let root = ReactDOM.createRoot(container, {hydrate: true});
979
root.render(
980
<Context.Provider value={{text: 'Hello', className: 'hello'}}>
981
<App />
@@ -1049,7 +1053,7 @@ describe('ReactDOMServerPartialHydration', () => {
1053
1054
// On the client we have the data available quickly for some reason.
1055
suspend = false;
1052
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1056
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1057
root.render(<App />);
1058
Scheduler.unstable_flushAll();
1059
jest.runAllTimers();
@@ -1105,7 +1109,7 @@ describe('ReactDOMServerPartialHydration', () => {
1109
1110
// On the client we have the data available quickly for some reason.
1111
suspend = false;
1108
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1112
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1113
root.render(<App />);
1114
Scheduler.unstable_flushAll();
1115
// This will have exceeded the suspended time so we should timeout.
@@ -1166,7 +1170,7 @@ describe('ReactDOMServerPartialHydration', () => {
1170
1171
// On the client we have the data available quickly for some reason.
1172
suspend = false;
1169
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1173
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1174
root.render(<App />);
1175
Scheduler.unstable_flushAll();
1176
// This will have exceeded the suspended time so we should timeout.
@@ -1242,7 +1246,7 @@ describe('ReactDOMServerPartialHydration', () => {
1246
1247
// Attempt to hydrate the content.
1248
suspend = false;
1245
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1249
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1250
root.render(<App />);
1251
Scheduler.unstable_flushAll();
1252
jest.runAllTimers();
@@ -1335,7 +1339,7 @@ describe('ReactDOMServerPartialHydration', () => {
1339
1340
// Attempt to hydrate the content.
1341
suspend = false;
1338
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1342
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1343
root.render(<App />);
1344
Scheduler.unstable_flushAll();
1345
jest.runAllTimers();
@@ -1413,7 +1417,7 @@ describe('ReactDOMServerPartialHydration', () => {
1417
1418
let spanB = container.getElementsByTagName('span')[1];
1419
1416
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1420
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1421
1422
suspend = true;
1423
act(() => {
@@ -1495,7 +1499,7 @@ describe('ReactDOMServerPartialHydration', () => {
1499
1500
let spanA = container.getElementsByTagName('span')[0];
1501
1498
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1502
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1503
1504
suspend = true;
1505
act(() => {
@@ -1575,7 +1579,7 @@ describe('ReactDOMServerPartialHydration', () => {
1579
// Put the suspense node in pending state.
1580
suspenseNode.data = '$?';
1581
1578
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1582
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1583
1584
suspend = true;
1585
act(() => {
@@ -1652,7 +1656,7 @@ describe('ReactDOMServerPartialHydration', () => {
1656
let span = container.getElementsByTagName('span')[1];
1657
1658
suspend = false;
1655
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1659
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1660
root.render(<App />);
1661
Scheduler.unstable_flushAll();
1662
jest.runAllTimers();
@@ -1695,7 +1699,7 @@ describe('ReactDOMServerPartialHydration', () => {
1699
1700
// On the client we don't have all data yet but we want to start
1701
// hydrating anyway.
1698
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1702
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1703
root.render(<App />);
1704
Scheduler.unstable_flushAll();
1705
jest.runAllTimers();
@@ -1748,7 +1752,7 @@ describe('ReactDOMServerPartialHydration', () => {
1752
// On the client we don't have all data yet but we want to start
1753
// hydrating anyway.
1754
suspend = true;
1751
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1755
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1756
root.render(
1757
<ClassName.Provider value={'hello'}>
1758
<App text="Hello" />
@@ -1840,7 +1844,7 @@ describe('ReactDOMServerPartialHydration', () => {
1844
// On the client we don't have all data yet but we want to start
1845
// hydrating anyway.
1846
suspend = true;
1843
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1847
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1848
root.render(<App />);
1849
Scheduler.unstable_flushAll();
1850
jest.runAllTimers();
@@ -1914,7 +1918,7 @@ describe('ReactDOMServerPartialHydration', () => {
1918
// On the client we don't have all data yet but we want to start
1919
// hydrating anyway.
1920
suspend = true;
1917
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
1921
+ let root = ReactDOM.createRoot(container, {hydrate: true});
1922
root.render(<App />);
1923
1924
// We'll do one click before hydrating.
@@ -1995,7 +1999,7 @@ describe('ReactDOMServerPartialHydration', () => {
1999
// On the client we don't have all data yet but we want to start
2000
// hydrating anyway.
2001
suspend = true;
1998
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2002
+ let root = ReactDOM.createRoot(container, {hydrate: true});
2003
root.render(<App />);
2004
2005
// We'll do one click before hydrating.
@@ -2072,7 +2076,7 @@ describe('ReactDOMServerPartialHydration', () => {
2076
// On the client we don't have all data yet but we want to start
2077
// hydrating anyway.
2078
suspend = true;
2075
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2079
+ let root = ReactDOM.createRoot(container, {hydrate: true});
2080
root.render(<App />);
2081
2082
// We'll do one click before hydrating.
@@ -2151,7 +2155,7 @@ describe('ReactDOMServerPartialHydration', () => {
2155
// On the client we don't have all data yet but we want to start
2156
// hydrating anyway.
2157
suspend = true;
2154
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2158
+ let root = ReactDOM.createRoot(container, {hydrate: true});
2159
root.render(<App />);
2160
Scheduler.unstable_flushAll();
2161
jest.runAllTimers();
@@ -2216,7 +2220,7 @@ describe('ReactDOMServerPartialHydration', () => {
2220
2221
// We're going to use a different root as a parent.
2222
// This lets us detect whether an event goes through React's event system.
2219
- let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
2223
+ let parentRoot = ReactDOM.createRoot(parentContainer);
2224
parentRoot.render(<Parent />);
2225
Scheduler.unstable_flushAll();
2226
@@ -2229,7 +2233,7 @@ describe('ReactDOMServerPartialHydration', () => {
2233
suspend = true;
2234
2235
// Hydrate asynchronously.
2232
- let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
2236
+ let root = ReactDOM.createRoot(childContainer, {hydrate: true});
2237
root.render(<App />);
2238
jest.runAllTimers();
2239
Scheduler.unstable_flushAll();
@@ -2319,7 +2323,7 @@ describe('ReactDOMServerPartialHydration', () => {
2323
// hydrating anyway.
2324
suspend1 = true;
2325
suspend2 = true;
2322
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2326
+ let root = ReactDOM.createRoot(container, {hydrate: true});
2327
root.render(<App />);
2328
2329
Scheduler.unstable_flushAll();
@@ -2434,7 +2438,7 @@ describe('ReactDOMServerPartialHydration', () => {
2438
// hydrating anyway.
2439
suspend1 = true;
2440
suspend2 = true;
2437
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
2441
+ let root = ReactDOM.createRoot(container, {hydrate: true});
2442
root.render(<App />);
2443
2444
Scheduler.unstable_flushAll();
packages/react-dom/src/__tests__/ReactDOMServerSelectiveHydration-test.internal.js
+10
-10
@@ -13,7 +13,6 @@ let React;
13
let ReactDOM;
14
let ReactDOMServer;
15
let Scheduler;
16
-let ReactFeatureFlags;
16
let Suspense;
17
18
function dispatchMouseHoverEvent(to, from) {
@@ -93,10 +92,6 @@ describe('ReactDOMServerSelectiveHydration', () => {
92
beforeEach(() => {
93
jest.resetModuleRegistry();
94
96
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
97
- ReactFeatureFlags.enableSuspenseServerRenderer = true;
98
- ReactFeatureFlags.enableSelectiveHydration = true;
99
-
95
React = require('react');
96
ReactDOM = require('react-dom');
97
ReactDOMServer = require('react-dom/server');
@@ -104,6 +99,11 @@ describe('ReactDOMServerSelectiveHydration', () => {
99
Suspense = React.Suspense;
100
});
101
102
+ if (!__EXPERIMENTAL__) {
103
+ it("empty test so Jest doesn't complain", () => {});
104
+ return;
105
+ }
106
+
107
it('hydrates the target boundary synchronously during a click', async () => {
108
function Child({text}) {
109
Scheduler.unstable_yieldValue(text);
@@ -144,7 +144,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
144
145
let span = container.getElementsByTagName('span')[1];
146
147
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
147
+ let root = ReactDOM.createRoot(container, {hydrate: true});
148
root.render(<App />);
149
150
// Nothing has been hydrated so far.
@@ -223,7 +223,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
223
224
// A and D will be suspended. We'll click on D which should take
225
// priority, after we unsuspend.
226
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
226
+ let root = ReactDOM.createRoot(container, {hydrate: true});
227
root.render(<App />);
228
229
// Nothing has been hydrated so far.
@@ -309,7 +309,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
309
310
// A and D will be suspended. We'll click on D which should take
311
// priority, after we unsuspend.
312
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
312
+ let root = ReactDOM.createRoot(container, {hydrate: true});
313
root.render(<App />);
314
315
// Nothing has been hydrated so far.
@@ -405,7 +405,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
405
406
// A and D will be suspended. We'll click on D which should take
407
// priority, after we unsuspend.
408
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
408
+ let root = ReactDOM.createRoot(container, {hydrate: true});
409
root.render(<App />);
410
411
// Nothing has been hydrated so far.
@@ -474,7 +474,7 @@ describe('ReactDOMServerSelectiveHydration', () => {
474
let spanB = container.getElementsByTagName('span')[1];
475
let spanC = container.getElementsByTagName('span')[2];
476
477
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
477
+ let root = ReactDOM.createRoot(container, {hydrate: true});
478
root.render(<App />);
479
480
// Nothing has been hydrated so far.
packages/react-dom/src/__tests__/ReactDOMServerSuspense-test.internal.js
+6
-5
@@ -14,16 +14,12 @@ const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegratio
14
let React;
15
let ReactDOM;
16
let ReactDOMServer;
17
-let ReactFeatureFlags;
17
let ReactTestUtils;
18
19
function initModules() {
20
// Reset warning cache.
21
jest.resetModuleRegistry();
22
24
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
25
- ReactFeatureFlags.enableSuspenseServerRenderer = true;
26
-
23
React = require('react');
24
ReactDOM = require('react-dom');
25
ReactDOMServer = require('react-dom/server');
@@ -48,6 +44,11 @@ describe('ReactDOMServerSuspense', () => {
44
resetModules();
45
});
46
47
+ if (!__EXPERIMENTAL__) {
48
+ it("empty test so Jest doesn't complain", () => {});
49
+ return;
50
+ }
51
+
52
function Text(props) {
53
return <div>{props.text}</div>;
54
}
@@ -125,7 +126,7 @@ describe('ReactDOMServerSuspense', () => {
126
expect(divB.textContent).toBe('B');
127
128
ReactTestUtils.act(() => {
128
- const root = ReactDOM.unstable_createSyncRoot(parent, {hydrate: true});
129
+ const root = ReactDOM.createSyncRoot(parent, {hydrate: true});
130
root.render(example);
131
});
132
packages/react-dom/src/__tests__/ReactServerRendering-test.js
+29
-27
@@ -682,38 +682,40 @@ describe('ReactDOMServer', () => {
682
expect(markup).toBe('<div></div>');
683
});
684
685
- it('throws for unsupported types on the server', () => {
686
- expect(() => {
687
- ReactDOMServer.renderToString(<React.Suspense />);
688
- }).toThrow('ReactDOMServer does not yet support Suspense.');
685
+ if (!__EXPERIMENTAL__) {
686
+ it('throws for unsupported types on the server', () => {
687
+ expect(() => {
688
+ ReactDOMServer.renderToString(<React.Suspense />);
689
+ }).toThrow('ReactDOMServer does not yet support Suspense.');
690
690
- async function fakeImport(result) {
691
- return {default: result};
692
- }
691
+ async function fakeImport(result) {
692
+ return {default: result};
693
+ }
694
694
- expect(() => {
695
- const LazyFoo = React.lazy(() =>
696
- fakeImport(
697
- new Promise(resolve =>
698
- resolve(function Foo() {
699
- return <div />;
700
- }),
695
+ expect(() => {
696
+ const LazyFoo = React.lazy(() =>
697
+ fakeImport(
698
+ new Promise(resolve =>
699
+ resolve(function Foo() {
700
+ return <div />;
701
+ }),
702
+ ),
703
),
702
- ),
703
- );
704
- ReactDOMServer.renderToString(<LazyFoo />);
705
- }).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
706
- });
704
+ );
705
+ ReactDOMServer.renderToString(<LazyFoo />);
706
+ }).toThrow('ReactDOMServer does not yet support lazy-loaded components.');
707
+ });
708
708
- it('throws when suspending on the server', () => {
709
- function AsyncFoo() {
710
- throw new Promise(() => {});
711
- }
709
+ it('throws when suspending on the server', () => {
710
+ function AsyncFoo() {
711
+ throw new Promise(() => {});
712
+ }
713
713
- expect(() => {
714
- ReactDOMServer.renderToString(<AsyncFoo />);
715
- }).toThrow('ReactDOMServer does not yet support Suspense.');
716
- });
714
+ expect(() => {
715
+ ReactDOMServer.renderToString(<AsyncFoo />);
716
+ }).toThrow('ReactDOMServer does not yet support Suspense.');
717
+ });
718
+ }
719
720
it('does not get confused by throwing null', () => {
721
function Bad() {
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+16
-159
@@ -500,165 +500,22 @@ describe('ReactDOMServerHydration', () => {
500
expect(element.textContent).toBe('Hello world');
501
});
502
503
- it('does not re-enter hydration after committing the first one', () => {
504
- let finalHTML = ReactDOMServer.renderToString(<div />);
505
- let container = document.createElement('div');
506
- container.innerHTML = finalHTML;
507
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
508
- root.render(<div />);
509
- Scheduler.unstable_flushAll();
510
- root.render(null);
511
- Scheduler.unstable_flushAll();
512
- // This should not reenter hydration state and therefore not trigger hydration
513
- // warnings.
514
- root.render(<div />);
515
- Scheduler.unstable_flushAll();
516
- });
517
-
518
- it('does not invoke an event on a concurrent hydrating node until it commits', () => {
519
- function Sibling({text}) {
520
- Scheduler.unstable_yieldValue('Sibling');
521
- return <span>Sibling</span>;
522
- }
523
-
524
- function Sibling2({text}) {
525
- Scheduler.unstable_yieldValue('Sibling2');
526
- return null;
527
- }
528
-
529
- let clicks = 0;
530
-
531
- function Button() {
532
- Scheduler.unstable_yieldValue('Button');
533
- let [clicked, setClicked] = React.useState(false);
534
- if (clicked) {
535
- return null;
536
- }
537
- return (
538
- <a
539
- onClick={() => {
540
- setClicked(true);
541
- clicks++;
542
- }}>
543
- Click me
544
- </a>
545
- );
546
- }
547
-
548
- function App() {
549
- return (
550
- <div>
551
- <Button />
552
- <Sibling />
553
- <Sibling2 />
554
- </div>
555
- );
556
- }
557
-
558
- let finalHTML = ReactDOMServer.renderToString(<App />);
559
- let container = document.createElement('div');
560
- container.innerHTML = finalHTML;
561
- expect(Scheduler).toHaveYielded(['Button', 'Sibling', 'Sibling2']);
562
-
563
- // We need this to be in the document since we'll dispatch events on it.
564
- document.body.appendChild(container);
565
-
566
- let a = container.getElementsByTagName('a')[0];
567
-
568
- // Hydrate asynchronously.
569
- let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
570
- root.render(<App />);
571
-
572
- // We haven't started hydrating yet.
573
- a.click();
574
- // Clicking should not invoke the event yet because we haven't committed
575
- // the hydration yet.
576
- expect(clicks).toBe(0);
577
-
578
- // Flush part way through the render.
579
- if (__DEV__) {
580
- // In DEV effects gets double invoked.
581
- expect(Scheduler).toFlushAndYieldThrough(['Button', 'Button', 'Sibling']);
582
- } else {
583
- expect(Scheduler).toFlushAndYieldThrough(['Button', 'Sibling']);
584
- }
585
-
586
- expect(container.textContent).toBe('Click meSibling');
587
-
588
- // We're now partially hydrated.
589
- a.click();
590
- // Clicking should not invoke the event yet because we haven't committed
591
- // the hydration yet.
592
- expect(clicks).toBe(0);
593
-
594
- // Finish the rest of the hydration.
595
- if (__DEV__) {
596
- // In DEV effects gets double invoked.
597
- expect(Scheduler).toFlushAndYield(['Sibling2', 'Button', 'Button']);
598
- } else {
599
- expect(Scheduler).toFlushAndYield(['Sibling2', 'Button']);
600
- }
601
-
602
- // We should have picked up both events now.
603
- expect(clicks).toBe(2);
604
-
605
- expect(container.textContent).toBe('Sibling');
606
-
607
- document.body.removeChild(container);
608
- });
609
-
610
- it('does not invoke an event on a parent tree when a subtree is hydrating', () => {
611
- let clicks = 0;
612
- let childSlotRef = React.createRef();
613
-
614
- function Parent() {
615
- return <div onClick={() => clicks++} ref={childSlotRef} />;
616
- }
617
-
618
- function App() {
619
- return (
620
- <div>
621
- <a>Click me</a>
622
- </div>
623
- );
624
- }
625
-
626
- let finalHTML = ReactDOMServer.renderToString(<App />);
627
-
628
- let parentContainer = document.createElement('div');
629
- let childContainer = document.createElement('div');
630
-
631
- // We need this to be in the document since we'll dispatch events on it.
632
- document.body.appendChild(parentContainer);
633
-
634
- // We're going to use a different root as a parent.
635
- // This lets us detect whether an event goes through React's event system.
636
- let parentRoot = ReactDOM.unstable_createRoot(parentContainer);
637
- parentRoot.render(<Parent />);
638
- Scheduler.unstable_flushAll();
639
-
640
- childSlotRef.current.appendChild(childContainer);
641
-
642
- childContainer.innerHTML = finalHTML;
643
-
644
- let a = childContainer.getElementsByTagName('a')[0];
645
-
646
- // Hydrate asynchronously.
647
- let root = ReactDOM.unstable_createRoot(childContainer, {hydrate: true});
648
- root.render(<App />);
649
- // Nothing has rendered so far.
650
-
651
- a.click();
652
- expect(clicks).toBe(0);
653
-
654
- Scheduler.unstable_flushAll();
655
-
656
- // We're now full hydrated.
657
-
658
- expect(clicks).toBe(1);
659
-
660
- document.body.removeChild(parentContainer);
661
- });
503
+ if (__EXPERIMENTAL__) {
504
+ it('does not re-enter hydration after committing the first one', () => {
505
+ let finalHTML = ReactDOMServer.renderToString(<div />);
506
+ let container = document.createElement('div');
507
+ container.innerHTML = finalHTML;
508
+ let root = ReactDOM.createRoot(container, {hydrate: true});
509
+ root.render(<div />);
510
+ Scheduler.unstable_flushAll();
511
+ root.render(null);
512
+ Scheduler.unstable_flushAll();
513
+ // This should not reenter hydration state and therefore not trigger hydration
514
+ // warnings.
515
+ root.render(<div />);
516
+ Scheduler.unstable_flushAll();
517
+ });
518
+ }
519
520
it('regression test: Suspense + hydration in legacy mode ', () => {
521
const element = document.createElement('div');
packages/react-dom/src/__tests__/ReactTestUtilsAct-test.js
+63
-61
@@ -27,30 +27,32 @@ function sleep(period) {
27
28
describe('ReactTestUtils.act()', () => {
29
// first we run all the tests with concurrent mode
30
- let concurrentRoot = null;
31
- function renderConcurrent(el, dom) {
32
- concurrentRoot = ReactDOM.unstable_createRoot(dom);
33
- concurrentRoot.render(el);
34
- }
35
-
36
- function unmountConcurrent(_dom) {
37
- if (concurrentRoot !== null) {
38
- concurrentRoot.unmount();
39
- concurrentRoot = null;
40
- }
41
- }
42
-
43
- function rerenderConcurrent(el) {
44
- concurrentRoot.render(el);
30
+ if (__EXPERIMENTAL__) {
31
+ let concurrentRoot = null;
32
+ const renderConcurrent = (el, dom) => {
33
+ concurrentRoot = ReactDOM.createRoot(dom);
34
+ concurrentRoot.render(el);
35
+ };
36
+
37
+ const unmountConcurrent = _dom => {
38
+ if (concurrentRoot !== null) {
39
+ concurrentRoot.unmount();
40
+ concurrentRoot = null;
41
+ }
42
+ };
43
+
44
+ const rerenderConcurrent = el => {
45
+ concurrentRoot.render(el);
46
+ };
47
+
48
+ runActTests(
49
+ 'concurrent mode',
50
+ renderConcurrent,
51
+ unmountConcurrent,
52
+ rerenderConcurrent,
53
+ );
54
}
55
47
- runActTests(
48
- 'concurrent mode',
49
- renderConcurrent,
50
- unmountConcurrent,
51
- rerenderConcurrent,
52
- );
53
-
56
// and then in sync mode
57
58
let syncDom = null;
@@ -71,25 +73,27 @@ describe('ReactTestUtils.act()', () => {
73
runActTests('legacy sync mode', renderSync, unmountSync, rerenderSync);
74
75
// and then in batched mode
74
- let batchedRoot = null;
75
- function renderBatched(el, dom) {
76
- batchedRoot = ReactDOM.unstable_createSyncRoot(dom);
77
- batchedRoot.render(el);
78
- }
76
+ if (__EXPERIMENTAL__) {
77
+ let batchedRoot = null;
78
+ const renderBatched = (el, dom) => {
79
+ batchedRoot = ReactDOM.createSyncRoot(dom);
80
+ batchedRoot.render(el);
81
+ };
82
+
83
+ const unmountBatched = dom => {
84
+ if (batchedRoot !== null) {
85
+ batchedRoot.unmount();
86
+ batchedRoot = null;
87
+ }
88
+ };
89
80
- function unmountBatched(dom) {
81
- if (batchedRoot !== null) {
82
- batchedRoot.unmount();
83
- batchedRoot = null;
84
- }
85
- }
90
+ const rerenderBatched = el => {
91
+ batchedRoot.render(el);
92
+ };
93
87
- function rerenderBatched(el) {
88
- batchedRoot.render(el);
94
+ runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched);
95
}
96
91
- runActTests('batched mode', renderBatched, unmountBatched, rerenderBatched);
92
-
97
describe('unacted effects', () => {
98
function App() {
99
React.useEffect(() => {}, []);
@@ -116,31 +120,29 @@ describe('ReactTestUtils.act()', () => {
120
]);
121
});
122
119
- it('warns in batched mode', () => {
120
- expect(() => {
121
- const root = ReactDOM.unstable_createSyncRoot(
122
- document.createElement('div'),
123
- );
124
- root.render(<App />);
125
- Scheduler.unstable_flushAll();
126
- }).toWarnDev([
127
- 'An update to App ran an effect, but was not wrapped in act(...)',
128
- 'An update to App ran an effect, but was not wrapped in act(...)',
129
- ]);
130
- });
123
+ if (__EXPERIMENTAL__) {
124
+ it('warns in batched mode', () => {
125
+ expect(() => {
126
+ const root = ReactDOM.createSyncRoot(document.createElement('div'));
127
+ root.render(<App />);
128
+ Scheduler.unstable_flushAll();
129
+ }).toWarnDev([
130
+ 'An update to App ran an effect, but was not wrapped in act(...)',
131
+ 'An update to App ran an effect, but was not wrapped in act(...)',
132
+ ]);
133
+ });
134
132
- it('warns in concurrent mode', () => {
133
- expect(() => {
134
- const root = ReactDOM.unstable_createRoot(
135
- document.createElement('div'),
136
- );
137
- root.render(<App />);
138
- Scheduler.unstable_flushAll();
139
- }).toWarnDev([
140
- 'An update to App ran an effect, but was not wrapped in act(...)',
141
- 'An update to App ran an effect, but was not wrapped in act(...)',
142
- ]);
143
- });
135
+ it('warns in concurrent mode', () => {
136
+ expect(() => {
137
+ const root = ReactDOM.createRoot(document.createElement('div'));
138
+ root.render(<App />);
139
+ Scheduler.unstable_flushAll();
140
+ }).toWarnDev([
141
+ 'An update to App ran an effect, but was not wrapped in act(...)',
142
+ 'An update to App ran an effect, but was not wrapped in act(...)',
143
+ ]);
144
+ });
145
+ }
146
});
147
});
148
packages/react-dom/src/__tests__/ReactUnmockedSchedulerWarning-test.js
+27
-29
@@ -27,34 +27,32 @@ it('does not warn when rendering in sync mode', () => {
27
}).toWarnDev([]);
28
});
29
30
-it('should warn when rendering in concurrent mode', () => {
31
- expect(() => {
32
- ReactDOM.unstable_createRoot(document.createElement('div')).render(<App />);
33
- }).toWarnDev(
34
- 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
35
- 'to guarantee consistent behaviour across tests and browsers.',
36
- {withoutStack: true},
37
- );
38
- // does not warn twice
39
- expect(() => {
40
- ReactDOM.unstable_createRoot(document.createElement('div')).render(<App />);
41
- }).toWarnDev([]);
42
-});
43
-
44
-it('should warn when rendering in batched mode', () => {
45
- expect(() => {
46
- ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
47
- <App />,
30
+if (__EXPERIMENTAL__) {
31
+ it('should warn when rendering in concurrent mode', () => {
32
+ expect(() => {
33
+ ReactDOM.createRoot(document.createElement('div')).render(<App />);
34
+ }).toWarnDev(
35
+ 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
36
+ 'to guarantee consistent behaviour across tests and browsers.',
37
+ {withoutStack: true},
38
);
49
- }).toWarnDev(
50
- 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
51
- 'to guarantee consistent behaviour across tests and browsers.',
52
- {withoutStack: true},
53
- );
54
- // does not warn twice
55
- expect(() => {
56
- ReactDOM.unstable_createSyncRoot(document.createElement('div')).render(
57
- <App />,
39
+ // does not warn twice
40
+ expect(() => {
41
+ ReactDOM.createRoot(document.createElement('div')).render(<App />);
42
+ }).toWarnDev([]);
43
+ });
44
+
45
+ it('should warn when rendering in batched mode', () => {
46
+ expect(() => {
47
+ ReactDOM.createSyncRoot(document.createElement('div')).render(<App />);
48
+ }).toWarnDev(
49
+ 'In Concurrent or Sync modes, the "scheduler" module needs to be mocked ' +
50
+ 'to guarantee consistent behaviour across tests and browsers.',
51
+ {withoutStack: true},
52
);
59
- }).toWarnDev([]);
60
-});
53
+ // does not warn twice
54
+ expect(() => {
55
+ ReactDOM.createSyncRoot(document.createElement('div')).render(<App />);
56
+ }).toWarnDev([]);
57
+ });
58
+}
packages/react-dom/src/__tests__/ReactUpdates-test.js
+66
-60
@@ -1292,78 +1292,84 @@ describe('ReactUpdates', () => {
1292
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1293
});
1294
1295
- it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
1296
- const container = document.createElement('div');
1295
+ if (__EXPERIMENTAL__) {
1296
+ it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
1297
+ const container = document.createElement('div');
1298
1298
- function Baz() {
1299
- Scheduler.unstable_yieldValue('Baz');
1300
- return <p>baz</p>;
1301
- }
1299
+ function Baz() {
1300
+ Scheduler.unstable_yieldValue('Baz');
1301
+ return <p>baz</p>;
1302
+ }
1303
1303
- let setCounter;
1304
- function Bar() {
1305
- const [counter, _setCounter] = React.useState(0);
1306
- setCounter = _setCounter;
1307
- Scheduler.unstable_yieldValue('Bar');
1308
- return <p>bar {counter}</p>;
1309
- }
1304
+ let setCounter;
1305
+ function Bar() {
1306
+ const [counter, _setCounter] = React.useState(0);
1307
+ setCounter = _setCounter;
1308
+ Scheduler.unstable_yieldValue('Bar');
1309
+ return <p>bar {counter}</p>;
1310
+ }
1311
1311
- function Foo() {
1312
- Scheduler.unstable_yieldValue('Foo');
1313
- React.useEffect(() => {
1314
- Scheduler.unstable_yieldValue('Foo#effect');
1315
- });
1316
- return (
1317
- <div>
1318
- <div hidden={true}>
1319
- <Bar />
1312
+ function Foo() {
1313
+ Scheduler.unstable_yieldValue('Foo');
1314
+ React.useEffect(() => {
1315
+ Scheduler.unstable_yieldValue('Foo#effect');
1316
+ });
1317
+ return (
1318
+ <div>
1319
+ <div hidden={true}>
1320
+ <Bar />
1321
+ </div>
1322
+ <Baz />
1323
</div>
1321
- <Baz />
1322
- </div>
1323
- );
1324
- }
1325
-
1326
- const root = ReactDOM.unstable_createRoot(container);
1327
- let hiddenDiv;
1328
- act(() => {
1329
- root.render(<Foo />);
1330
- if (__DEV__) {
1331
- expect(Scheduler).toFlushAndYieldThrough([
1332
- 'Foo',
1333
- 'Foo',
1334
- 'Baz',
1335
- 'Foo#effect',
1336
- ]);
1337
- } else {
1338
- expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
1324
+ );
1325
}
1340
- hiddenDiv = container.firstChild.firstChild;
1341
- expect(hiddenDiv.hidden).toBe(true);
1342
- expect(hiddenDiv.innerHTML).toBe('');
1326
+
1327
+ const root = ReactDOM.createRoot(container);
1328
+ let hiddenDiv;
1329
+ act(() => {
1330
+ root.render(<Foo />);
1331
+ if (__DEV__) {
1332
+ expect(Scheduler).toFlushAndYieldThrough([
1333
+ 'Foo',
1334
+ 'Foo',
1335
+ 'Baz',
1336
+ 'Foo#effect',
1337
+ ]);
1338
+ } else {
1339
+ expect(Scheduler).toFlushAndYieldThrough([
1340
+ 'Foo',
1341
+ 'Baz',
1342
+ 'Foo#effect',
1343
+ ]);
1344
+ }
1345
+ hiddenDiv = container.firstChild.firstChild;
1346
+ expect(hiddenDiv.hidden).toBe(true);
1347
+ expect(hiddenDiv.innerHTML).toBe('');
1348
+ // Run offscreen update
1349
+ if (__DEV__) {
1350
+ expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1351
+ } else {
1352
+ expect(Scheduler).toFlushAndYield(['Bar']);
1353
+ }
1354
+ expect(hiddenDiv.hidden).toBe(true);
1355
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1356
+ });
1357
+
1358
+ ReactDOM.flushSync(() => {
1359
+ setCounter(1);
1360
+ });
1361
+ // Should not flush yet
1362
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1363
+
1364
// Run offscreen update
1365
if (__DEV__) {
1366
expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1367
} else {
1368
expect(Scheduler).toFlushAndYield(['Bar']);
1369
}
1349
- expect(hiddenDiv.hidden).toBe(true);
1350
- expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1370
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
1371
});
1352
-
1353
- ReactDOM.flushSync(() => {
1354
- setCounter(1);
1355
- });
1356
- // Should not flush yet
1357
- expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1358
-
1359
- // Run offscreen update
1360
- if (__DEV__) {
1361
- expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1362
- } else {
1363
- expect(Scheduler).toFlushAndYield(['Bar']);
1364
- }
1365
- expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
1366
- });
1372
+ }
1373
1374
it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
1375
class Foo extends React.Component {
packages/react-dom/src/client/ReactDOM.js
+12
-21
@@ -61,7 +61,7 @@ import getComponentName from 'shared/getComponentName';
61
import invariant from 'shared/invariant';
62
import lowPriorityWarningWithoutStack from 'shared/lowPriorityWarningWithoutStack';
63
import warningWithoutStack from 'shared/warningWithoutStack';
64
-import {enableStableConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
64
+import {exposeConcurrentModeAPIs} from 'shared/ReactFeatureFlags';
65
66
import {
67
getInstanceFromNode,
@@ -593,27 +593,8 @@ const ReactDOM: Object = {
593
594
unstable_batchedUpdates: batchedUpdates,
595
596
- // TODO remove this legacy method, unstable_discreteUpdates replaces it
597
- unstable_interactiveUpdates: (fn, a, b, c) => {
598
- flushDiscreteUpdates();
599
- return discreteUpdates(fn, a, b, c);
600
- },
601
-
602
- unstable_discreteUpdates: discreteUpdates,
603
- unstable_flushDiscreteUpdates: flushDiscreteUpdates,
604
-
596
flushSync: flushSync,
597
607
- unstable_createRoot: createRoot,
608
- unstable_createSyncRoot: createSyncRoot,
609
- unstable_flushControlled: flushControlled,
610
-
611
- unstable_scheduleHydration(target: Node) {
612
- if (target) {
613
- queueExplicitHydrationTarget(target);
614
- }
615
- },
616
-
598
__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED: {
599
// Keep in sync with ReactDOMUnstableNativeDependencies.js
600
// ReactTestUtils.js, and ReactTestUtilsAct.js. This is an array for better minification.
@@ -678,9 +659,19 @@ function warnIfReactDOMContainerInDEV(container) {
659
}
660
}
661
681
-if (enableStableConcurrentModeAPIs) {
662
+if (exposeConcurrentModeAPIs) {
663
ReactDOM.createRoot = createRoot;
664
ReactDOM.createSyncRoot = createSyncRoot;
665
+
666
+ ReactDOM.unstable_discreteUpdates = discreteUpdates;
667
+ ReactDOM.unstable_flushDiscreteUpdates = flushDiscreteUpdates;
668
+ ReactDOM.unstable_flushControlled = flushControlled;
669
+
670
+ ReactDOM.unstable_scheduleHydration = target => {
671
+ if (target) {
672
+ queueExplicitHydrationTarget(target);
673
+ }
674
+ };
675
}
676
677
const foundDevTools = injectIntoDevTools({
packages/react-dom/src/events/__tests__/ChangeEventPlugin-test.internal.js
+282
-280
@@ -474,319 +474,321 @@ describe('ChangeEventPlugin', () => {
474
}
475
});
476
477
- describe('concurrent mode', () => {
478
- beforeEach(() => {
479
- jest.resetModules();
480
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
481
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
482
- React = require('react');
483
- ReactDOM = require('react-dom');
484
- TestUtils = require('react-dom/test-utils');
485
- Scheduler = require('scheduler');
486
- });
477
+ if (__EXPERIMENTAL__) {
478
+ describe('concurrent mode', () => {
479
+ beforeEach(() => {
480
+ jest.resetModules();
481
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
482
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
483
+ React = require('react');
484
+ ReactDOM = require('react-dom');
485
+ TestUtils = require('react-dom/test-utils');
486
+ Scheduler = require('scheduler');
487
+ });
488
488
- it('text input', () => {
489
- const root = ReactDOM.unstable_createRoot(container);
490
- let input;
489
+ it('text input', () => {
490
+ const root = ReactDOM.createRoot(container);
491
+ let input;
492
492
- let ops = [];
493
+ let ops = [];
494
494
- class ControlledInput extends React.Component {
495
- state = {value: 'initial'};
496
- onChange = event => this.setState({value: event.target.value});
497
- render() {
498
- ops.push(`render: ${this.state.value}`);
499
- const controlledValue =
500
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
501
- return (
502
- <input
503
- ref={el => (input = el)}
504
- type="text"
505
- value={controlledValue}
506
- onChange={this.onChange}
507
- />
508
- );
495
+ class ControlledInput extends React.Component {
496
+ state = {value: 'initial'};
497
+ onChange = event => this.setState({value: event.target.value});
498
+ render() {
499
+ ops.push(`render: ${this.state.value}`);
500
+ const controlledValue =
501
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
502
+ return (
503
+ <input
504
+ ref={el => (input = el)}
505
+ type="text"
506
+ value={controlledValue}
507
+ onChange={this.onChange}
508
+ />
509
+ );
510
+ }
511
}
510
- }
512
512
- // Initial mount. Test that this is async.
513
- root.render(<ControlledInput />);
514
- // Should not have flushed yet.
515
- expect(ops).toEqual([]);
516
- expect(input).toBe(undefined);
517
- // Flush callbacks.
518
- Scheduler.unstable_flushAll();
519
- expect(ops).toEqual(['render: initial']);
520
- expect(input.value).toBe('initial');
521
-
522
- ops = [];
523
-
524
- // Trigger a change event.
525
- setUntrackedValue.call(input, 'changed');
526
- input.dispatchEvent(
527
- new Event('input', {bubbles: true, cancelable: true}),
528
- );
529
- // Change should synchronously flush
530
- expect(ops).toEqual(['render: changed']);
531
- // Value should be the controlled value, not the original one
532
- expect(input.value).toBe('changed [!]');
533
- });
513
+ // Initial mount. Test that this is async.
514
+ root.render(<ControlledInput />);
515
+ // Should not have flushed yet.
516
+ expect(ops).toEqual([]);
517
+ expect(input).toBe(undefined);
518
+ // Flush callbacks.
519
+ Scheduler.unstable_flushAll();
520
+ expect(ops).toEqual(['render: initial']);
521
+ expect(input.value).toBe('initial');
522
+
523
+ ops = [];
524
+
525
+ // Trigger a change event.
526
+ setUntrackedValue.call(input, 'changed');
527
+ input.dispatchEvent(
528
+ new Event('input', {bubbles: true, cancelable: true}),
529
+ );
530
+ // Change should synchronously flush
531
+ expect(ops).toEqual(['render: changed']);
532
+ // Value should be the controlled value, not the original one
533
+ expect(input.value).toBe('changed [!]');
534
+ });
535
535
- it('checkbox input', () => {
536
- const root = ReactDOM.unstable_createRoot(container);
537
- let input;
538
-
539
- let ops = [];
540
-
541
- class ControlledInput extends React.Component {
542
- state = {checked: false};
543
- onChange = event => {
544
- this.setState({checked: event.target.checked});
545
- };
546
- render() {
547
- ops.push(`render: ${this.state.checked}`);
548
- const controlledValue = this.props.reverse
549
- ? !this.state.checked
550
- : this.state.checked;
551
- return (
552
- <input
553
- ref={el => (input = el)}
554
- type="checkbox"
555
- checked={controlledValue}
556
- onChange={this.onChange}
557
- />
558
- );
536
+ it('checkbox input', () => {
537
+ const root = ReactDOM.createRoot(container);
538
+ let input;
539
+
540
+ let ops = [];
541
+
542
+ class ControlledInput extends React.Component {
543
+ state = {checked: false};
544
+ onChange = event => {
545
+ this.setState({checked: event.target.checked});
546
+ };
547
+ render() {
548
+ ops.push(`render: ${this.state.checked}`);
549
+ const controlledValue = this.props.reverse
550
+ ? !this.state.checked
551
+ : this.state.checked;
552
+ return (
553
+ <input
554
+ ref={el => (input = el)}
555
+ type="checkbox"
556
+ checked={controlledValue}
557
+ onChange={this.onChange}
558
+ />
559
+ );
560
+ }
561
}
560
- }
561
-
562
- // Initial mount. Test that this is async.
563
- root.render(<ControlledInput reverse={false} />);
564
- // Should not have flushed yet.
565
- expect(ops).toEqual([]);
566
- expect(input).toBe(undefined);
567
- // Flush callbacks.
568
- Scheduler.unstable_flushAll();
569
- expect(ops).toEqual(['render: false']);
570
- expect(input.checked).toBe(false);
571
-
572
- ops = [];
573
-
574
- // Trigger a change event.
575
- input.dispatchEvent(
576
- new MouseEvent('click', {bubbles: true, cancelable: true}),
577
- );
578
- // Change should synchronously flush
579
- expect(ops).toEqual(['render: true']);
580
- expect(input.checked).toBe(true);
562
582
- // Now let's make sure we're using the controlled value.
583
- root.render(<ControlledInput reverse={true} />);
584
- Scheduler.unstable_flushAll();
585
-
586
- ops = [];
563
+ // Initial mount. Test that this is async.
564
+ root.render(<ControlledInput reverse={false} />);
565
+ // Should not have flushed yet.
566
+ expect(ops).toEqual([]);
567
+ expect(input).toBe(undefined);
568
+ // Flush callbacks.
569
+ Scheduler.unstable_flushAll();
570
+ expect(ops).toEqual(['render: false']);
571
+ expect(input.checked).toBe(false);
572
+
573
+ ops = [];
574
+
575
+ // Trigger a change event.
576
+ input.dispatchEvent(
577
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
578
+ );
579
+ // Change should synchronously flush
580
+ expect(ops).toEqual(['render: true']);
581
+ expect(input.checked).toBe(true);
582
588
- // Trigger another change event.
589
- input.dispatchEvent(
590
- new MouseEvent('click', {bubbles: true, cancelable: true}),
591
- );
592
- // Change should synchronously flush
593
- expect(ops).toEqual(['render: true']);
594
- expect(input.checked).toBe(false);
595
- });
583
+ // Now let's make sure we're using the controlled value.
584
+ root.render(<ControlledInput reverse={true} />);
585
+ Scheduler.unstable_flushAll();
586
597
- it('textarea', () => {
598
- const root = ReactDOM.unstable_createRoot(container);
599
- let textarea;
587
+ ops = [];
588
601
- let ops = [];
589
+ // Trigger another change event.
590
+ input.dispatchEvent(
591
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
592
+ );
593
+ // Change should synchronously flush
594
+ expect(ops).toEqual(['render: true']);
595
+ expect(input.checked).toBe(false);
596
+ });
597
603
- class ControlledTextarea extends React.Component {
604
- state = {value: 'initial'};
605
- onChange = event => this.setState({value: event.target.value});
606
- render() {
607
- ops.push(`render: ${this.state.value}`);
608
- const controlledValue =
609
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
610
- return (
611
- <textarea
612
- ref={el => (textarea = el)}
613
- type="text"
614
- value={controlledValue}
615
- onChange={this.onChange}
616
- />
617
- );
598
+ it('textarea', () => {
599
+ const root = ReactDOM.createRoot(container);
600
+ let textarea;
601
+
602
+ let ops = [];
603
+
604
+ class ControlledTextarea extends React.Component {
605
+ state = {value: 'initial'};
606
+ onChange = event => this.setState({value: event.target.value});
607
+ render() {
608
+ ops.push(`render: ${this.state.value}`);
609
+ const controlledValue =
610
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
611
+ return (
612
+ <textarea
613
+ ref={el => (textarea = el)}
614
+ type="text"
615
+ value={controlledValue}
616
+ onChange={this.onChange}
617
+ />
618
+ );
619
+ }
620
}
619
- }
621
621
- // Initial mount. Test that this is async.
622
- root.render(<ControlledTextarea />);
623
- // Should not have flushed yet.
624
- expect(ops).toEqual([]);
625
- expect(textarea).toBe(undefined);
626
- // Flush callbacks.
627
- Scheduler.unstable_flushAll();
628
- expect(ops).toEqual(['render: initial']);
629
- expect(textarea.value).toBe('initial');
630
-
631
- ops = [];
632
-
633
- // Trigger a change event.
634
- setUntrackedTextareaValue.call(textarea, 'changed');
635
- textarea.dispatchEvent(
636
- new Event('input', {bubbles: true, cancelable: true}),
637
- );
638
- // Change should synchronously flush
639
- expect(ops).toEqual(['render: changed']);
640
- // Value should be the controlled value, not the original one
641
- expect(textarea.value).toBe('changed [!]');
642
- });
622
+ // Initial mount. Test that this is async.
623
+ root.render(<ControlledTextarea />);
624
+ // Should not have flushed yet.
625
+ expect(ops).toEqual([]);
626
+ expect(textarea).toBe(undefined);
627
+ // Flush callbacks.
628
+ Scheduler.unstable_flushAll();
629
+ expect(ops).toEqual(['render: initial']);
630
+ expect(textarea.value).toBe('initial');
631
+
632
+ ops = [];
633
+
634
+ // Trigger a change event.
635
+ setUntrackedTextareaValue.call(textarea, 'changed');
636
+ textarea.dispatchEvent(
637
+ new Event('input', {bubbles: true, cancelable: true}),
638
+ );
639
+ // Change should synchronously flush
640
+ expect(ops).toEqual(['render: changed']);
641
+ // Value should be the controlled value, not the original one
642
+ expect(textarea.value).toBe('changed [!]');
643
+ });
644
644
- it('parent of input', () => {
645
- const root = ReactDOM.unstable_createRoot(container);
646
- let input;
645
+ it('parent of input', () => {
646
+ const root = ReactDOM.createRoot(container);
647
+ let input;
648
+
649
+ let ops = [];
650
+
651
+ class ControlledInput extends React.Component {
652
+ state = {value: 'initial'};
653
+ onChange = event => this.setState({value: event.target.value});
654
+ render() {
655
+ ops.push(`render: ${this.state.value}`);
656
+ const controlledValue =
657
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
658
+ return (
659
+ <div onChange={this.onChange}>
660
+ <input
661
+ ref={el => (input = el)}
662
+ type="text"
663
+ value={controlledValue}
664
+ onChange={() => {
665
+ // Does nothing. Parent handler is responsible for updating.
666
+ }}
667
+ />
668
+ </div>
669
+ );
670
+ }
671
+ }
672
648
- let ops = [];
673
+ // Initial mount. Test that this is async.
674
+ root.render(<ControlledInput />);
675
+ // Should not have flushed yet.
676
+ expect(ops).toEqual([]);
677
+ expect(input).toBe(undefined);
678
+ // Flush callbacks.
679
+ Scheduler.unstable_flushAll();
680
+ expect(ops).toEqual(['render: initial']);
681
+ expect(input.value).toBe('initial');
682
+
683
+ ops = [];
684
+
685
+ // Trigger a change event.
686
+ setUntrackedValue.call(input, 'changed');
687
+ input.dispatchEvent(
688
+ new Event('input', {bubbles: true, cancelable: true}),
689
+ );
690
+ // Change should synchronously flush
691
+ expect(ops).toEqual(['render: changed']);
692
+ // Value should be the controlled value, not the original one
693
+ expect(input.value).toBe('changed [!]');
694
+ });
695
650
- class ControlledInput extends React.Component {
651
- state = {value: 'initial'};
652
- onChange = event => this.setState({value: event.target.value});
653
- render() {
654
- ops.push(`render: ${this.state.value}`);
655
- const controlledValue =
656
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
657
- return (
658
- <div onChange={this.onChange}>
696
+ it('is async for non-input events', () => {
697
+ const root = ReactDOM.createRoot(container);
698
+ let input;
699
+
700
+ let ops = [];
701
+
702
+ class ControlledInput extends React.Component {
703
+ state = {value: 'initial'};
704
+ onChange = event => this.setState({value: event.target.value});
705
+ reset = () => {
706
+ this.setState({value: ''});
707
+ };
708
+ render() {
709
+ ops.push(`render: ${this.state.value}`);
710
+ const controlledValue =
711
+ this.state.value === 'changed' ? 'changed [!]' : this.state.value;
712
+ return (
713
<input
714
ref={el => (input = el)}
715
type="text"
716
value={controlledValue}
663
- onChange={() => {
664
- // Does nothing. Parent handler is responsible for updating.
665
- }}
717
+ onChange={this.onChange}
718
+ onClick={this.reset}
719
/>
667
- </div>
668
- );
720
+ );
721
+ }
722
}
670
- }
723
672
- // Initial mount. Test that this is async.
673
- root.render(<ControlledInput />);
674
- // Should not have flushed yet.
675
- expect(ops).toEqual([]);
676
- expect(input).toBe(undefined);
677
- // Flush callbacks.
678
- Scheduler.unstable_flushAll();
679
- expect(ops).toEqual(['render: initial']);
680
- expect(input.value).toBe('initial');
724
+ // Initial mount. Test that this is async.
725
+ root.render(<ControlledInput />);
726
+ // Should not have flushed yet.
727
+ expect(ops).toEqual([]);
728
+ expect(input).toBe(undefined);
729
+ // Flush callbacks.
730
+ Scheduler.unstable_flushAll();
731
+ expect(ops).toEqual(['render: initial']);
732
+ expect(input.value).toBe('initial');
733
+
734
+ ops = [];
735
+
736
+ // Trigger a click event
737
+ input.dispatchEvent(
738
+ new Event('click', {bubbles: true, cancelable: true}),
739
+ );
740
+ // Nothing should have changed
741
+ expect(ops).toEqual([]);
742
+ expect(input.value).toBe('initial');
743
+
744
+ // Flush callbacks.
745
+ Scheduler.unstable_flushAll();
746
+ // Now the click update has flushed.
747
+ expect(ops).toEqual(['render: ']);
748
+ expect(input.value).toBe('');
749
+ });
750
682
- ops = [];
751
+ it('mouse enter/leave should be user-blocking but not discrete', async () => {
752
+ // This is currently behind a feature flag
753
+ jest.resetModules();
754
+ React = require('react');
755
+ ReactDOM = require('react-dom');
756
+ TestUtils = require('react-dom/test-utils');
757
+ Scheduler = require('scheduler');
758
684
- // Trigger a change event.
685
- setUntrackedValue.call(input, 'changed');
686
- input.dispatchEvent(
687
- new Event('input', {bubbles: true, cancelable: true}),
688
- );
689
- // Change should synchronously flush
690
- expect(ops).toEqual(['render: changed']);
691
- // Value should be the controlled value, not the original one
692
- expect(input.value).toBe('changed [!]');
693
- });
759
+ const {act} = TestUtils;
760
+ const {useState} = React;
761
695
- it('is async for non-input events', () => {
696
- const root = ReactDOM.unstable_createRoot(container);
697
- let input;
698
-
699
- let ops = [];
700
-
701
- class ControlledInput extends React.Component {
702
- state = {value: 'initial'};
703
- onChange = event => this.setState({value: event.target.value});
704
- reset = () => {
705
- this.setState({value: ''});
706
- };
707
- render() {
708
- ops.push(`render: ${this.state.value}`);
709
- const controlledValue =
710
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
762
+ const root = ReactDOM.createRoot(container);
763
+
764
+ const target = React.createRef(null);
765
+ function Foo() {
766
+ const [isHover, setHover] = useState(false);
767
return (
712
- <input
713
- ref={el => (input = el)}
714
- type="text"
715
- value={controlledValue}
716
- onChange={this.onChange}
717
- onClick={this.reset}
718
- />
768
+ <div
769
+ ref={target}
770
+ onMouseEnter={() => setHover(true)}
771
+ onMouseLeave={() => setHover(false)}>
772
+ {isHover ? 'hovered' : 'not hovered'}
773
+ </div>
774
);
775
}
721
- }
722
-
723
- // Initial mount. Test that this is async.
724
- root.render(<ControlledInput />);
725
- // Should not have flushed yet.
726
- expect(ops).toEqual([]);
727
- expect(input).toBe(undefined);
728
- // Flush callbacks.
729
- Scheduler.unstable_flushAll();
730
- expect(ops).toEqual(['render: initial']);
731
- expect(input.value).toBe('initial');
732
-
733
- ops = [];
776
735
- // Trigger a click event
736
- input.dispatchEvent(
737
- new Event('click', {bubbles: true, cancelable: true}),
738
- );
739
- // Nothing should have changed
740
- expect(ops).toEqual([]);
741
- expect(input.value).toBe('initial');
742
-
743
- // Flush callbacks.
744
- Scheduler.unstable_flushAll();
745
- // Now the click update has flushed.
746
- expect(ops).toEqual(['render: ']);
747
- expect(input.value).toBe('');
748
- });
749
-
750
- it('mouse enter/leave should be user-blocking but not discrete', async () => {
751
- // This is currently behind a feature flag
752
- jest.resetModules();
753
- React = require('react');
754
- ReactDOM = require('react-dom');
755
- TestUtils = require('react-dom/test-utils');
756
- Scheduler = require('scheduler');
757
-
758
- const {act} = TestUtils;
759
- const {useState} = React;
760
-
761
- const root = ReactDOM.unstable_createRoot(container);
762
-
763
- const target = React.createRef(null);
764
- function Foo() {
765
- const [isHover, setHover] = useState(false);
766
- return (
767
- <div
768
- ref={target}
769
- onMouseEnter={() => setHover(true)}
770
- onMouseLeave={() => setHover(false)}>
771
- {isHover ? 'hovered' : 'not hovered'}
772
- </div>
773
- );
774
- }
775
-
776
- await act(async () => {
777
- root.render(<Foo />);
778
- });
779
- expect(container.textContent).toEqual('not hovered');
777
+ await act(async () => {
778
+ root.render(<Foo />);
779
+ });
780
+ expect(container.textContent).toEqual('not hovered');
781
781
- await act(async () => {
782
- const mouseOverEvent = document.createEvent('MouseEvents');
783
- mouseOverEvent.initEvent('mouseover', true, true);
784
- target.current.dispatchEvent(mouseOverEvent);
782
+ await act(async () => {
783
+ const mouseOverEvent = document.createEvent('MouseEvents');
784
+ mouseOverEvent.initEvent('mouseover', true, true);
785
+ target.current.dispatchEvent(mouseOverEvent);
786
786
- // 3s should be enough to expire the updates
787
- Scheduler.unstable_advanceTime(3000);
788
- expect(container.textContent).toEqual('hovered');
787
+ // 3s should be enough to expire the updates
788
+ Scheduler.unstable_advanceTime(3000);
789
+ expect(container.textContent).toEqual('hovered');
790
+ });
791
});
792
});
791
- });
793
+ }
794
});
packages/react-dom/src/events/__tests__/DOMEventResponderSystem-test.internal.js
+43
-41
@@ -845,54 +845,56 @@ describe('DOMEventResponderSystem', () => {
845
buttonRef.current.dispatchEvent(createEvent('foobar'));
846
});
847
848
- it('should work with concurrent mode updates', async () => {
849
- const log = [];
850
- const TestResponder = createEventResponder({
851
- targetEventTypes: ['click'],
852
- onEvent(event, context, props) {
853
- log.push(props);
854
- },
855
- });
856
- const ref = React.createRef();
857
-
858
- function Test({counter}) {
859
- const listener = React.unstable_useResponder(TestResponder, {counter});
860
- Scheduler.unstable_yieldValue('Test');
861
- return (
862
- <button listeners={listener} ref={ref}>
863
- Press me
864
- </button>
865
- );
866
- }
848
+ if (__EXPERIMENTAL__) {
849
+ it('should work with concurrent mode updates', async () => {
850
+ const log = [];
851
+ const TestResponder = createEventResponder({
852
+ targetEventTypes: ['click'],
853
+ onEvent(event, context, props) {
854
+ log.push(props);
855
+ },
856
+ });
857
+ const ref = React.createRef();
858
+
859
+ function Test({counter}) {
860
+ const listener = React.unstable_useResponder(TestResponder, {counter});
861
+ Scheduler.unstable_yieldValue('Test');
862
+ return (
863
+ <button listeners={listener} ref={ref}>
864
+ Press me
865
+ </button>
866
+ );
867
+ }
868
868
- let root = ReactDOM.unstable_createRoot(container);
869
- root.render(<Test counter={0} />);
870
- expect(Scheduler).toFlushAndYield(['Test']);
869
+ let root = ReactDOM.createRoot(container);
870
+ root.render(<Test counter={0} />);
871
+ expect(Scheduler).toFlushAndYield(['Test']);
872
872
- // Click the button
873
- dispatchClickEvent(ref.current);
874
- expect(log).toEqual([{counter: 0}]);
873
+ // Click the button
874
+ dispatchClickEvent(ref.current);
875
+ expect(log).toEqual([{counter: 0}]);
876
876
- // Clear log
877
- log.length = 0;
877
+ // Clear log
878
+ log.length = 0;
879
879
- // Increase counter
880
- root.render(<Test counter={1} />);
881
- // Yield before committing
882
- expect(Scheduler).toFlushAndYieldThrough(['Test']);
880
+ // Increase counter
881
+ root.render(<Test counter={1} />);
882
+ // Yield before committing
883
+ expect(Scheduler).toFlushAndYieldThrough(['Test']);
884
884
- // Click the button again
885
- dispatchClickEvent(ref.current);
886
- expect(log).toEqual([{counter: 0}]);
885
+ // Click the button again
886
+ dispatchClickEvent(ref.current);
887
+ expect(log).toEqual([{counter: 0}]);
888
888
- // Clear log
889
- log.length = 0;
889
+ // Clear log
890
+ log.length = 0;
891
891
- // Commit
892
- expect(Scheduler).toFlushAndYield([]);
893
- dispatchClickEvent(ref.current);
894
- expect(log).toEqual([{counter: 1}]);
895
- });
892
+ // Commit
893
+ expect(Scheduler).toFlushAndYield([]);
894
+ dispatchClickEvent(ref.current);
895
+ expect(log).toEqual([{counter: 1}]);
896
+ });
897
+ }
898
899
it('should correctly pass through event properties', () => {
900
const timeStamps = [];
packages/react-dom/src/events/__tests__/SimpleEventPlugin-test.internal.js
+230
-220
@@ -230,246 +230,256 @@ describe('SimpleEventPlugin', function() {
230
expect(button.textContent).toEqual('Count: 3');
231
});
232
233
- describe('interactive events, in concurrent mode', () => {
234
- beforeEach(() => {
235
- jest.resetModules();
236
- ReactFeatureFlags = require('shared/ReactFeatureFlags');
237
- ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
238
- ReactDOM = require('react-dom');
239
- Scheduler = require('scheduler');
240
- });
233
+ if (__EXPERIMENTAL__) {
234
+ describe('interactive events, in concurrent mode', () => {
235
+ beforeEach(() => {
236
+ jest.resetModules();
237
+ ReactFeatureFlags = require('shared/ReactFeatureFlags');
238
+ ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
239
+ ReactDOM = require('react-dom');
240
+ Scheduler = require('scheduler');
241
+ });
242
242
- it('flushes pending interactive work before extracting event handler', () => {
243
- container = document.createElement('div');
244
- const root = ReactDOM.unstable_createRoot(container);
245
- document.body.appendChild(container);
246
-
247
- let ops = [];
248
-
249
- let button;
250
- class Button extends React.Component {
251
- state = {disabled: false};
252
- onClick = () => {
253
- // Perform some side-effect
254
- ops.push('Side-effect');
255
- // Disable the button
256
- this.setState({disabled: true});
257
- };
258
- render() {
259
- ops.push(
260
- `render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
261
- );
262
- return (
263
- <button
264
- ref={el => (button = el)}
265
- // Handler is removed after the first click
266
- onClick={this.state.disabled ? null : this.onClick}
267
- />
268
- );
243
+ it('flushes pending interactive work before extracting event handler', () => {
244
+ container = document.createElement('div');
245
+ const root = ReactDOM.createRoot(container);
246
+ document.body.appendChild(container);
247
+
248
+ let ops = [];
249
+
250
+ let button;
251
+ class Button extends React.Component {
252
+ state = {disabled: false};
253
+ onClick = () => {
254
+ // Perform some side-effect
255
+ ops.push('Side-effect');
256
+ // Disable the button
257
+ this.setState({disabled: true});
258
+ };
259
+ render() {
260
+ ops.push(
261
+ `render button: ${this.state.disabled ? 'disabled' : 'enabled'}`,
262
+ );
263
+ return (
264
+ <button
265
+ ref={el => (button = el)}
266
+ // Handler is removed after the first click
267
+ onClick={this.state.disabled ? null : this.onClick}
268
+ />
269
+ );
270
+ }
271
}
270
- }
272
272
- // Initial mount
273
- root.render(<Button />);
274
- // Should not have flushed yet because it's async
275
- expect(ops).toEqual([]);
276
- expect(button).toBe(undefined);
277
- // Flush async work
278
- Scheduler.unstable_flushAll();
279
- expect(ops).toEqual(['render button: enabled']);
273
+ // Initial mount
274
+ root.render(<Button />);
275
+ // Should not have flushed yet because it's async
276
+ expect(ops).toEqual([]);
277
+ expect(button).toBe(undefined);
278
+ // Flush async work
279
+ Scheduler.unstable_flushAll();
280
+ expect(ops).toEqual(['render button: enabled']);
281
281
- ops = [];
282
+ ops = [];
283
283
- function click() {
284
- button.dispatchEvent(
285
- new MouseEvent('click', {bubbles: true, cancelable: true}),
286
- );
287
- }
284
+ function click() {
285
+ button.dispatchEvent(
286
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
287
+ );
288
+ }
289
289
- // Click the button to trigger the side-effect
290
- click();
291
- expect(ops).toEqual([
292
- // The handler fired
293
- 'Side-effect',
294
- // but the component did not re-render yet, because it's async
295
- ]);
296
-
297
- ops = [];
298
-
299
- // Click the button again
300
- click();
301
- expect(ops).toEqual([
302
- // Before handling this second click event, the previous interactive
303
- // update is flushed
304
- 'render button: disabled',
305
- // The event handler was removed from the button, so there's no second
306
- // side-effect
307
- ]);
308
-
309
- ops = [];
310
-
311
- // The handler should not fire again no matter how many times we
312
- // click the handler.
313
- click();
314
- click();
315
- click();
316
- click();
317
- click();
318
- Scheduler.unstable_flushAll();
319
- expect(ops).toEqual([]);
320
- });
290
+ // Click the button to trigger the side-effect
291
+ click();
292
+ expect(ops).toEqual([
293
+ // The handler fired
294
+ 'Side-effect',
295
+ // but the component did not re-render yet, because it's async
296
+ ]);
297
+
298
+ ops = [];
299
+
300
+ // Click the button again
301
+ click();
302
+ expect(ops).toEqual([
303
+ // Before handling this second click event, the previous interactive
304
+ // update is flushed
305
+ 'render button: disabled',
306
+ // The event handler was removed from the button, so there's no second
307
+ // side-effect
308
+ ]);
309
+
310
+ ops = [];
311
+
312
+ // The handler should not fire again no matter how many times we
313
+ // click the handler.
314
+ click();
315
+ click();
316
+ click();
317
+ click();
318
+ click();
319
+ Scheduler.unstable_flushAll();
320
+ expect(ops).toEqual([]);
321
+ });
322
322
- it('end result of many interactive updates is deterministic', () => {
323
- container = document.createElement('div');
324
- const root = ReactDOM.unstable_createRoot(container);
325
- document.body.appendChild(container);
326
-
327
- let button;
328
- class Button extends React.Component {
329
- state = {count: 0};
330
- render() {
331
- return (
332
- <button
333
- ref={el => (button = el)}
334
- onClick={() =>
335
- // Intentionally not using the updater form here
336
- this.setState({count: this.state.count + 1})
337
- }>
338
- Count: {this.state.count}
339
- </button>
323
+ it('end result of many interactive updates is deterministic', () => {
324
+ container = document.createElement('div');
325
+ const root = ReactDOM.createRoot(container);
326
+ document.body.appendChild(container);
327
+
328
+ let button;
329
+ class Button extends React.Component {
330
+ state = {count: 0};
331
+ render() {
332
+ return (
333
+ <button
334
+ ref={el => (button = el)}
335
+ onClick={() =>
336
+ // Intentionally not using the updater form here
337
+ this.setState({count: this.state.count + 1})
338
+ }>
339
+ Count: {this.state.count}
340
+ </button>
341
+ );
342
+ }
343
+ }
344
+
345
+ // Initial mount
346
+ root.render(<Button />);
347
+ // Should not have flushed yet because it's async
348
+ expect(button).toBe(undefined);
349
+ // Flush async work
350
+ Scheduler.unstable_flushAll();
351
+ expect(button.textContent).toEqual('Count: 0');
352
+
353
+ function click() {
354
+ button.dispatchEvent(
355
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
356
);
357
}
342
- }
358
344
- // Initial mount
345
- root.render(<Button />);
346
- // Should not have flushed yet because it's async
347
- expect(button).toBe(undefined);
348
- // Flush async work
349
- Scheduler.unstable_flushAll();
350
- expect(button.textContent).toEqual('Count: 0');
351
-
352
- function click() {
353
- button.dispatchEvent(
354
- new MouseEvent('click', {bubbles: true, cancelable: true}),
355
- );
356
- }
359
+ // Click the button a single time
360
+ click();
361
+ // The counter should not have updated yet because it's async
362
+ expect(button.textContent).toEqual('Count: 0');
363
+
364
+ // Click the button many more times
365
+ click();
366
+ click();
367
+ click();
368
+ click();
369
+ click();
370
+ click();
371
+
372
+ // Flush the remaining work
373
+ Scheduler.unstable_flushAll();
374
+ // The counter should equal the total number of clicks
375
+ expect(button.textContent).toEqual('Count: 7');
376
+ });
377
358
- // Click the button a single time
359
- click();
360
- // The counter should not have updated yet because it's async
361
- expect(button.textContent).toEqual('Count: 0');
362
-
363
- // Click the button many more times
364
- click();
365
- click();
366
- click();
367
- click();
368
- click();
369
- click();
370
-
371
- // Flush the remaining work
372
- Scheduler.unstable_flushAll();
373
- // The counter should equal the total number of clicks
374
- expect(button.textContent).toEqual('Count: 7');
375
- });
378
+ it('flushes discrete updates in order', () => {
379
+ container = document.createElement('div');
380
+ document.body.appendChild(container);
381
377
- it('flushes discrete updates in order', () => {
378
- container = document.createElement('div');
379
- document.body.appendChild(container);
380
-
381
- let button;
382
- class Button extends React.Component {
383
- state = {lowPriCount: 0};
384
- render() {
385
- const text = `High-pri count: ${
386
- this.props.highPriCount
387
- }, Low-pri count: ${this.state.lowPriCount}`;
388
- Scheduler.unstable_yieldValue(text);
389
- return (
390
- <button
391
- ref={el => (button = el)}
392
- onClick={() => {
393
- Scheduler.unstable_next(() => {
394
- this.setState(state => ({
395
- lowPriCount: state.lowPriCount + 1,
396
- }));
397
- });
398
- }}>
399
- {text}
400
- </button>
401
- );
382
+ let button;
383
+ class Button extends React.Component {
384
+ state = {lowPriCount: 0};
385
+ render() {
386
+ const text = `High-pri count: ${
387
+ this.props.highPriCount
388
+ }, Low-pri count: ${this.state.lowPriCount}`;
389
+ Scheduler.unstable_yieldValue(text);
390
+ return (
391
+ <button
392
+ ref={el => (button = el)}
393
+ onClick={() => {
394
+ Scheduler.unstable_next(() => {
395
+ this.setState(state => ({
396
+ lowPriCount: state.lowPriCount + 1,
397
+ }));
398
+ });
399
+ }}>
400
+ {text}
401
+ </button>
402
+ );
403
+ }
404
}
403
- }
405
405
- class Wrapper extends React.Component {
406
- state = {highPriCount: 0};
407
- render() {
408
- return (
409
- <div
410
- onClick={
411
- // Intentionally not using the updater form here, to test
412
- // that updates are serially processed.
413
- () => {
414
- this.setState({highPriCount: this.state.highPriCount + 1});
415
- }
416
- }>
417
- <Button highPriCount={this.state.highPriCount} />
418
- </div>
406
+ class Wrapper extends React.Component {
407
+ state = {highPriCount: 0};
408
+ render() {
409
+ return (
410
+ <div
411
+ onClick={
412
+ // Intentionally not using the updater form here, to test
413
+ // that updates are serially processed.
414
+ () => {
415
+ this.setState({highPriCount: this.state.highPriCount + 1});
416
+ }
417
+ }>
418
+ <Button highPriCount={this.state.highPriCount} />
419
+ </div>
420
+ );
421
+ }
422
+ }
423
+
424
+ // Initial mount
425
+ const root = ReactDOM.createRoot(container);
426
+ root.render(<Wrapper />);
427
+ expect(Scheduler).toFlushAndYield([
428
+ 'High-pri count: 0, Low-pri count: 0',
429
+ ]);
430
+ expect(button.textContent).toEqual(
431
+ 'High-pri count: 0, Low-pri count: 0',
432
+ );
433
+
434
+ function click() {
435
+ button.dispatchEvent(
436
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
437
);
438
}
421
- }
439
423
- // Initial mount
424
- const root = ReactDOM.unstable_createRoot(container);
425
- root.render(<Wrapper />);
426
- expect(Scheduler).toFlushAndYield([
427
- 'High-pri count: 0, Low-pri count: 0',
428
- ]);
429
- expect(button.textContent).toEqual('High-pri count: 0, Low-pri count: 0');
430
-
431
- function click() {
432
- button.dispatchEvent(
433
- new MouseEvent('click', {bubbles: true, cancelable: true}),
440
+ // Click the button a single time
441
+ click();
442
+ // Nothing should flush on the first click.
443
+ expect(Scheduler).toHaveYielded([]);
444
+ // Click again. This will force the previous discrete update to flush. But
445
+ // only the high-pri count will increase.
446
+ click();
447
+ expect(Scheduler).toHaveYielded([
448
+ 'High-pri count: 1, Low-pri count: 0',
449
+ ]);
450
+ expect(button.textContent).toEqual(
451
+ 'High-pri count: 1, Low-pri count: 0',
452
);
435
- }
453
437
- // Click the button a single time
438
- click();
439
- // Nothing should flush on the first click.
440
- expect(Scheduler).toHaveYielded([]);
441
- // Click again. This will force the previous discrete update to flush. But
442
- // only the high-pri count will increase.
443
- click();
444
- expect(Scheduler).toHaveYielded(['High-pri count: 1, Low-pri count: 0']);
445
- expect(button.textContent).toEqual('High-pri count: 1, Low-pri count: 0');
446
-
447
- // Click the button many more times
448
- click();
449
- click();
450
- click();
451
- click();
452
- click();
453
- click();
454
-
455
- // Flush the remaining work.
456
- expect(Scheduler).toHaveYielded([
457
- 'High-pri count: 2, Low-pri count: 0',
458
- 'High-pri count: 3, Low-pri count: 0',
459
- 'High-pri count: 4, Low-pri count: 0',
460
- 'High-pri count: 5, Low-pri count: 0',
461
- 'High-pri count: 6, Low-pri count: 0',
462
- 'High-pri count: 7, Low-pri count: 0',
463
- ]);
464
-
465
- // At the end, both counters should equal the total number of clicks
466
- expect(Scheduler).toFlushAndYield([
467
- 'High-pri count: 8, Low-pri count: 0',
468
- 'High-pri count: 8, Low-pri count: 8',
469
- ]);
470
- expect(button.textContent).toEqual('High-pri count: 8, Low-pri count: 8');
454
+ // Click the button many more times
455
+ click();
456
+ click();
457
+ click();
458
+ click();
459
+ click();
460
+ click();
461
+
462
+ // Flush the remaining work.
463
+ expect(Scheduler).toHaveYielded([
464
+ 'High-pri count: 2, Low-pri count: 0',
465
+ 'High-pri count: 3, Low-pri count: 0',
466
+ 'High-pri count: 4, Low-pri count: 0',
467
+ 'High-pri count: 5, Low-pri count: 0',
468
+ 'High-pri count: 6, Low-pri count: 0',
469
+ 'High-pri count: 7, Low-pri count: 0',
470
+ ]);
471
+
472
+ // At the end, both counters should equal the total number of clicks
473
+ expect(Scheduler).toFlushAndYield([
474
+ 'High-pri count: 8, Low-pri count: 0',
475
+ 'High-pri count: 8, Low-pri count: 8',
476
+ ]);
477
+ expect(button.textContent).toEqual(
478
+ 'High-pri count: 8, Low-pri count: 8',
479
+ );
480
+ });
481
});
472
- });
482
+ }
483
484
describe('iOS bubbling click fix', function() {
485
// See http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
packages/react-interactions/events/src/dom/__tests__/Input-test.internal.js
+175
-169
@@ -750,202 +750,208 @@ describe('Input event responder', () => {
750
}
751
});
752
753
- describe('concurrent mode', () => {
754
- it('text input', () => {
755
- const root = ReactDOM.unstable_createRoot(container);
756
- let input;
757
-
758
- let ops = [];
759
-
760
- function Component({innerRef, onChange, controlledValue}) {
761
- const listener = useInput({
762
- onChange,
763
- });
764
- return (
765
- <input
766
- type="text"
767
- ref={innerRef}
768
- value={controlledValue}
769
- listeners={listener}
770
- />
771
- );
772
- }
773
-
774
- class ControlledInput extends React.Component {
775
- state = {value: 'initial'};
776
- onChange = event => this.setState({value: event.target.value});
777
- render() {
778
- ops.push(`render: ${this.state.value}`);
779
- const controlledValue =
780
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
753
+ if (__EXPERIMENTAL__) {
754
+ describe('concurrent mode', () => {
755
+ it('text input', () => {
756
+ const root = ReactDOM.createRoot(container);
757
+ let input;
758
+
759
+ let ops = [];
760
+
761
+ function Component({innerRef, onChange, controlledValue}) {
762
+ const listener = useInput({
763
+ onChange,
764
+ });
765
return (
782
- <Component
783
- onChange={this.onChange}
784
- innerRef={el => (input = el)}
785
- controlledValue={controlledValue}
766
+ <input
767
+ type="text"
768
+ ref={innerRef}
769
+ value={controlledValue}
770
+ listeners={listener}
771
/>
772
);
773
}
789
- }
774
791
- // Initial mount. Test that this is async.
792
- root.render(<ControlledInput />);
793
- // Should not have flushed yet.
794
- expect(ops).toEqual([]);
795
- expect(input).toBe(undefined);
796
- // Flush callbacks.
797
- Scheduler.unstable_flushAll();
798
- expect(ops).toEqual(['render: initial']);
799
- expect(input.value).toBe('initial');
800
-
801
- ops = [];
802
-
803
- // Trigger a change event.
804
- setUntrackedValue.call(input, 'changed');
805
- input.dispatchEvent(
806
- new Event('input', {bubbles: true, cancelable: true}),
807
- );
808
- // Change should synchronously flush
809
- expect(ops).toEqual(['render: changed']);
810
- // Value should be the controlled value, not the original one
811
- expect(input.value).toBe('changed [!]');
812
- });
775
+ class ControlledInput extends React.Component {
776
+ state = {value: 'initial'};
777
+ onChange = event => this.setState({value: event.target.value});
778
+ render() {
779
+ ops.push(`render: ${this.state.value}`);
780
+ const controlledValue =
781
+ this.state.value === 'changed'
782
+ ? 'changed [!]'
783
+ : this.state.value;
784
+ return (
785
+ <Component
786
+ onChange={this.onChange}
787
+ innerRef={el => (input = el)}
788
+ controlledValue={controlledValue}
789
+ />
790
+ );
791
+ }
792
+ }
793
814
- it('checkbox input', () => {
815
- const root = ReactDOM.unstable_createRoot(container);
816
- let input;
794
+ // Initial mount. Test that this is async.
795
+ root.render(<ControlledInput />);
796
+ // Should not have flushed yet.
797
+ expect(ops).toEqual([]);
798
+ expect(input).toBe(undefined);
799
+ // Flush callbacks.
800
+ Scheduler.unstable_flushAll();
801
+ expect(ops).toEqual(['render: initial']);
802
+ expect(input.value).toBe('initial');
803
+
804
+ ops = [];
805
+
806
+ // Trigger a change event.
807
+ setUntrackedValue.call(input, 'changed');
808
+ input.dispatchEvent(
809
+ new Event('input', {bubbles: true, cancelable: true}),
810
+ );
811
+ // Change should synchronously flush
812
+ expect(ops).toEqual(['render: changed']);
813
+ // Value should be the controlled value, not the original one
814
+ expect(input.value).toBe('changed [!]');
815
+ });
816
818
- let ops = [];
817
+ it('checkbox input', () => {
818
+ const root = ReactDOM.createRoot(container);
819
+ let input;
820
820
- function Component({innerRef, onChange, controlledValue}) {
821
- const listener = useInput({
822
- onChange,
823
- });
824
- return (
825
- <input
826
- type="checkbox"
827
- ref={innerRef}
828
- checked={controlledValue}
829
- listeners={listener}
830
- />
831
- );
832
- }
821
+ let ops = [];
822
834
- class ControlledInput extends React.Component {
835
- state = {checked: false};
836
- onChange = event => {
837
- this.setState({checked: event.target.checked});
838
- };
839
- render() {
840
- ops.push(`render: ${this.state.checked}`);
841
- const controlledValue = this.props.reverse
842
- ? !this.state.checked
843
- : this.state.checked;
823
+ function Component({innerRef, onChange, controlledValue}) {
824
+ const listener = useInput({
825
+ onChange,
826
+ });
827
return (
845
- <Component
846
- controlledValue={controlledValue}
847
- onChange={this.onChange}
848
- innerRef={el => (input = el)}
828
+ <input
829
+ type="checkbox"
830
+ ref={innerRef}
831
+ checked={controlledValue}
832
+ listeners={listener}
833
/>
834
);
835
}
852
- }
836
854
- // Initial mount. Test that this is async.
855
- root.render(<ControlledInput reverse={false} />);
856
- // Should not have flushed yet.
857
- expect(ops).toEqual([]);
858
- expect(input).toBe(undefined);
859
- // Flush callbacks.
860
- Scheduler.unstable_flushAll();
861
- expect(ops).toEqual(['render: false']);
862
- expect(input.checked).toBe(false);
863
-
864
- ops = [];
865
-
866
- // Trigger a change event.
867
- input.dispatchEvent(
868
- new MouseEvent('click', {bubbles: true, cancelable: true}),
869
- );
870
- // Change should synchronously flush
871
- expect(ops).toEqual(['render: true']);
872
- expect(input.checked).toBe(true);
837
+ class ControlledInput extends React.Component {
838
+ state = {checked: false};
839
+ onChange = event => {
840
+ this.setState({checked: event.target.checked});
841
+ };
842
+ render() {
843
+ ops.push(`render: ${this.state.checked}`);
844
+ const controlledValue = this.props.reverse
845
+ ? !this.state.checked
846
+ : this.state.checked;
847
+ return (
848
+ <Component
849
+ controlledValue={controlledValue}
850
+ onChange={this.onChange}
851
+ innerRef={el => (input = el)}
852
+ />
853
+ );
854
+ }
855
+ }
856
874
- // Now let's make sure we're using the controlled value.
875
- root.render(<ControlledInput reverse={true} />);
876
- Scheduler.unstable_flushAll();
857
+ // Initial mount. Test that this is async.
858
+ root.render(<ControlledInput reverse={false} />);
859
+ // Should not have flushed yet.
860
+ expect(ops).toEqual([]);
861
+ expect(input).toBe(undefined);
862
+ // Flush callbacks.
863
+ Scheduler.unstable_flushAll();
864
+ expect(ops).toEqual(['render: false']);
865
+ expect(input.checked).toBe(false);
866
+
867
+ ops = [];
868
+
869
+ // Trigger a change event.
870
+ input.dispatchEvent(
871
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
872
+ );
873
+ // Change should synchronously flush
874
+ expect(ops).toEqual(['render: true']);
875
+ expect(input.checked).toBe(true);
876
878
- ops = [];
877
+ // Now let's make sure we're using the controlled value.
878
+ root.render(<ControlledInput reverse={true} />);
879
+ Scheduler.unstable_flushAll();
880
880
- // Trigger another change event.
881
- input.dispatchEvent(
882
- new MouseEvent('click', {bubbles: true, cancelable: true}),
883
- );
884
- // Change should synchronously flush
885
- expect(ops).toEqual(['render: true']);
886
- expect(input.checked).toBe(false);
887
- });
881
+ ops = [];
882
889
- it('textarea', () => {
890
- const root = ReactDOM.unstable_createRoot(container);
891
- let textarea;
883
+ // Trigger another change event.
884
+ input.dispatchEvent(
885
+ new MouseEvent('click', {bubbles: true, cancelable: true}),
886
+ );
887
+ // Change should synchronously flush
888
+ expect(ops).toEqual(['render: true']);
889
+ expect(input.checked).toBe(false);
890
+ });
891
893
- let ops = [];
892
+ it('textarea', () => {
893
+ const root = ReactDOM.createRoot(container);
894
+ let textarea;
895
895
- function Component({innerRef, onChange, controlledValue}) {
896
- const listener = useInput({
897
- onChange,
898
- });
899
- return (
900
- <textarea
901
- type="text"
902
- ref={innerRef}
903
- value={controlledValue}
904
- listeners={listener}
905
- />
906
- );
907
- }
896
+ let ops = [];
897
909
- class ControlledTextarea extends React.Component {
910
- state = {value: 'initial'};
911
- onChange = event => this.setState({value: event.target.value});
912
- render() {
913
- ops.push(`render: ${this.state.value}`);
914
- const controlledValue =
915
- this.state.value === 'changed' ? 'changed [!]' : this.state.value;
898
+ function Component({innerRef, onChange, controlledValue}) {
899
+ const listener = useInput({
900
+ onChange,
901
+ });
902
return (
917
- <Component
918
- onChange={this.onChange}
919
- innerRef={el => (textarea = el)}
920
- controlledValue={controlledValue}
903
+ <textarea
904
+ type="text"
905
+ ref={innerRef}
906
+ value={controlledValue}
907
+ listeners={listener}
908
/>
909
);
910
}
924
- }
911
926
- // Initial mount. Test that this is async.
927
- root.render(<ControlledTextarea />);
928
- // Should not have flushed yet.
929
- expect(ops).toEqual([]);
930
- expect(textarea).toBe(undefined);
931
- // Flush callbacks.
932
- Scheduler.unstable_flushAll();
933
- expect(ops).toEqual(['render: initial']);
934
- expect(textarea.value).toBe('initial');
935
-
936
- ops = [];
937
-
938
- // Trigger a change event.
939
- setUntrackedTextareaValue.call(textarea, 'changed');
940
- textarea.dispatchEvent(
941
- new Event('input', {bubbles: true, cancelable: true}),
942
- );
943
- // Change should synchronously flush
944
- expect(ops).toEqual(['render: changed']);
945
- // Value should be the controlled value, not the original one
946
- expect(textarea.value).toBe('changed [!]');
912
+ class ControlledTextarea extends React.Component {
913
+ state = {value: 'initial'};
914
+ onChange = event => this.setState({value: event.target.value});
915
+ render() {
916
+ ops.push(`render: ${this.state.value}`);
917
+ const controlledValue =
918
+ this.state.value === 'changed'
919
+ ? 'changed [!]'
920
+ : this.state.value;
921
+ return (
922
+ <Component
923
+ onChange={this.onChange}
924
+ innerRef={el => (textarea = el)}
925
+ controlledValue={controlledValue}
926
+ />
927
+ );
928
+ }
929
+ }
930
+
931
+ // Initial mount. Test that this is async.
932
+ root.render(<ControlledTextarea />);
933
+ // Should not have flushed yet.
934
+ expect(ops).toEqual([]);
935
+ expect(textarea).toBe(undefined);
936
+ // Flush callbacks.
937
+ Scheduler.unstable_flushAll();
938
+ expect(ops).toEqual(['render: initial']);
939
+ expect(textarea.value).toBe('initial');
940
+
941
+ ops = [];
942
+
943
+ // Trigger a change event.
944
+ setUntrackedTextareaValue.call(textarea, 'changed');
945
+ textarea.dispatchEvent(
946
+ new Event('input', {bubbles: true, cancelable: true}),
947
+ );
948
+ // Change should synchronously flush
949
+ expect(ops).toEqual(['render: changed']);
950
+ // Value should be the controlled value, not the original one
951
+ expect(textarea.value).toBe('changed [!]');
952
+ });
953
});
948
- });
954
+ }
955
});
956
957
it('expect displayName to show up for event component', () => {
packages/react-interactions/events/src/dom/__tests__/MixedResponders-test-internal.js
+9
-4
@@ -34,6 +34,11 @@ describe('mixing responders with the heritage event system', () => {
34
container = null;
35
});
36
37
+ if (!__EXPERIMENTAL__) {
38
+ it("empty test so Jest doesn't complain", () => {});
39
+ return;
40
+ }
41
+
42
it('should properly only flush sync once when the event systems are mixed', () => {
43
const useTap = require('react-interactions/events/tap').useTap;
44
const ref = React.createRef();
@@ -66,7 +71,7 @@ describe('mixing responders with the heritage event system', () => {
71
}
72
73
const newContainer = document.createElement('div');
69
- const root = ReactDOM.unstable_createRoot(newContainer);
74
+ const root = ReactDOM.createRoot(newContainer);
75
document.body.appendChild(newContainer);
76
root.render(<MyComponent />);
77
Scheduler.unstable_flushAll();
@@ -137,7 +142,7 @@ describe('mixing responders with the heritage event system', () => {
142
}
143
144
const newContainer = document.createElement('div');
140
- const root = ReactDOM.unstable_createRoot(newContainer);
145
+ const root = ReactDOM.createRoot(newContainer);
146
document.body.appendChild(newContainer);
147
root.render(<MyComponent />);
148
Scheduler.unstable_flushAll();
@@ -216,7 +221,7 @@ describe('mixing responders with the heritage event system', () => {
221
222
const newContainer = document.createElement('div');
223
document.body.appendChild(newContainer);
219
- const root = ReactDOM.unstable_createRoot(newContainer);
224
+ const root = ReactDOM.createRoot(newContainer);
225
226
root.render(<MyComponent />);
227
Scheduler.unstable_flushAll();
@@ -238,7 +243,7 @@ describe('mixing responders with the heritage event system', () => {
243
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
244
const useTap = require('react-interactions/events/tap').useTap;
245
const useInput = require('react-interactions/events/input').useInput;
241
- const root = ReactDOM.unstable_createRoot(container);
246
+ const root = ReactDOM.createRoot(container);
247
let input;
248
249
let ops = [];
packages/react-refresh/src/__tests__/ReactFresh-test.js
+2
-2
@@ -2394,7 +2394,7 @@ describe('ReactFresh', () => {
2394
});
2395
2396
it('can hot reload offscreen components', () => {
2397
- if (__DEV__) {
2397
+ if (__DEV__ && __EXPERIMENTAL__) {
2398
const AppV1 = prepare(() => {
2399
function Hello() {
2400
React.useLayoutEffect(() => {
@@ -2421,7 +2421,7 @@ describe('ReactFresh', () => {
2421
};
2422
});
2423
2424
- const root = ReactDOM.unstable_createRoot(container);
2424
+ const root = ReactDOM.createRoot(container);
2425
root.render(<AppV1 offscreen={true} />);
2426
expect(Scheduler).toFlushAndYieldThrough(['App#layout']);
2427
const el = container.firstChild;
packages/react/src/__tests__/ReactDOMTracing-test.internal.js
+14
-10
@@ -27,7 +27,6 @@ function loadModules() {
27
ReactFeatureFlags = require('shared/ReactFeatureFlags');
28
ReactFeatureFlags.debugRenderPhaseSideEffects = false;
29
ReactFeatureFlags.debugRenderPhaseSideEffectsForStrictMode = false;
30
- ReactFeatureFlags.enableSuspenseServerRenderer = true;
30
ReactFeatureFlags.enableProfilerTimer = true;
31
ReactFeatureFlags.enableSchedulerTracing = true;
32
ReactFeatureFlags.replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
@@ -64,6 +63,11 @@ describe('ReactDOMTracing', () => {
63
loadModules();
64
});
65
66
+ if (!__EXPERIMENTAL__) {
67
+ it("empty test so Jest doesn't complain", () => {});
68
+ return;
69
+ }
70
+
71
describe('interaction tracing', () => {
72
describe('hidden', () => {
73
it('traces interaction through hidden subtree', () => {
@@ -101,7 +105,7 @@ describe('ReactDOMTracing', () => {
105
const onRender = jest.fn();
106
107
const container = document.createElement('div');
104
- const root = ReactDOM.unstable_createRoot(container);
108
+ const root = ReactDOM.createRoot(container);
109
SchedulerTracing.unstable_trace('initialization', 0, () => {
110
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
111
TestUtils.act(() => {
@@ -171,7 +175,7 @@ describe('ReactDOMTracing', () => {
175
const onRender = jest.fn();
176
177
const container = document.createElement('div');
174
- const root = ReactDOM.unstable_createRoot(container);
178
+ const root = ReactDOM.createRoot(container);
179
SchedulerTracing.unstable_trace('initialization', 0, () => {
180
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
181
@@ -250,7 +254,7 @@ describe('ReactDOMTracing', () => {
254
const onRender = jest.fn();
255
256
const container = document.createElement('div');
253
- const root = ReactDOM.unstable_createRoot(container);
257
+ const root = ReactDOM.createRoot(container);
258
SchedulerTracing.unstable_trace('initialization', 0, () => {
259
interaction = Array.from(SchedulerTracing.unstable_getCurrent())[0];
260
TestUtils.act(() => {
@@ -344,7 +348,7 @@ describe('ReactDOMTracing', () => {
348
349
const onRender = jest.fn();
350
const container = document.createElement('div');
347
- const root = ReactDOM.unstable_createRoot(container);
351
+ const root = ReactDOM.createRoot(container);
352
353
// Schedule some idle work without any interactions.
354
TestUtils.act(() => {
@@ -448,7 +452,7 @@ describe('ReactDOMTracing', () => {
452
453
const onRender = jest.fn();
454
const container = document.createElement('div');
451
- const root = ReactDOM.unstable_createRoot(container);
455
+ const root = ReactDOM.createRoot(container);
456
457
TestUtils.act(() => {
458
root.render(
@@ -545,7 +549,7 @@ describe('ReactDOMTracing', () => {
549
}
550
551
const container = document.createElement('div');
548
- const root = ReactDOM.unstable_createRoot(container);
552
+ const root = ReactDOM.createRoot(container);
553
554
let interaction;
555
@@ -627,7 +631,7 @@ describe('ReactDOMTracing', () => {
631
632
let interaction;
633
630
- const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
634
+ const root = ReactDOM.createRoot(container, {hydrate: true});
635
636
// Hydrate it.
637
SchedulerTracing.unstable_trace('initialization', 0, () => {
@@ -686,7 +690,7 @@ describe('ReactDOMTracing', () => {
690
691
let interaction;
692
689
- const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
693
+ const root = ReactDOM.createRoot(container, {hydrate: true});
694
695
// Start hydrating but simulate blocking for suspense data.
696
suspend = true;
@@ -755,7 +759,7 @@ describe('ReactDOMTracing', () => {
759
760
let interaction;
761
758
- const root = ReactDOM.unstable_createRoot(container, {hydrate: true});
762
+ const root = ReactDOM.createRoot(container, {hydrate: true});
763
764
// Hydrate without suspending to fill in the client-rendered content.
765
suspend = false;
packages/react/src/__tests__/ReactProfilerDOM-test.internal.js
+6
-1
@@ -54,6 +54,11 @@ describe('ProfilerDOM', () => {
54
return props.text;
55
}
56
57
+ if (!__EXPERIMENTAL__) {
58
+ it("empty test so Jest doesn't complain", () => {});
59
+ return;
60
+ }
61
+
62
it('should correctly trace interactions for async roots', async () => {
63
let resolve;
64
let thenable = {
@@ -75,7 +80,7 @@ describe('ProfilerDOM', () => {
80
}
81
82
const element = document.createElement('div');
78
- const root = ReactDOM.unstable_createRoot(element);
83
+ const root = ReactDOM.createRoot(element);
84
85
let interaction;
86
let wrappedResolve;
packages/shared/ReactFeatureFlags.js
+4
-4
@@ -31,9 +31,9 @@ export const enableProfilerTimer = __PROFILE__;
31
// Trace which interactions trigger each commit.
32
export const enableSchedulerTracing = __PROFILE__;
33
34
-// Only used in www builds.
35
-export const enableSuspenseServerRenderer = false; // TODO: __DEV__? Here it might just be false.
36
-export const enableSelectiveHydration = false;
34
+// SSR experiments
35
+export const enableSuspenseServerRenderer = __EXPERIMENTAL__;
36
+export const enableSelectiveHydration = __EXPERIMENTAL__;
37
38
// Only used in www builds.
39
export const enableSchedulerDebugging = false;
@@ -52,7 +52,7 @@ export const disableInputAttributeSyncing = false;
52
53
// These APIs will no longer be "unstable" in the upcoming 16.7 release,
54
// Control this behavior with a flag to support 16.6 minor releases in the meanwhile.
55
-export const enableStableConcurrentModeAPIs = __EXPERIMENTAL__;
55
+export const exposeConcurrentModeAPIs = __EXPERIMENTAL__;
56
57
export const warnAboutShorthandPropertyCollision = false;
58
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
-1
@@ -23,7 +23,7 @@ export const enableProfilerTimer = __PROFILE__;
23
export const enableSchedulerTracing = __PROFILE__;
24
export const enableSuspenseServerRenderer = false;
25
export const enableSelectiveHydration = false;
26
-export const enableStableConcurrentModeAPIs = false;
26
+export const exposeConcurrentModeAPIs = false;
27
export const warnAboutShorthandPropertyCollision = false;
28
export const enableSchedulerDebugging = false;
29
export const debugRenderPhaseSideEffectsForStrictMode = true;
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
-1
@@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
-export const enableStableConcurrentModeAPIs = false;
26
+export const exposeConcurrentModeAPIs = false;
27
export const warnAboutShorthandPropertyCollision = false;
28
export const enableSchedulerDebugging = false;
29
export const enableFlareAPI = false;
packages/shared/forks/ReactFeatureFlags.persistent.js
+1
-1
@@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
-export const enableStableConcurrentModeAPIs = false;
26
+export const exposeConcurrentModeAPIs = false;
27
export const warnAboutShorthandPropertyCollision = false;
28
export const enableSchedulerDebugging = false;
29
export const enableFlareAPI = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
-1
@@ -23,7 +23,7 @@ export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
24
export const disableJavaScriptURLs = false;
25
export const disableInputAttributeSyncing = false;
26
-export const enableStableConcurrentModeAPIs = false;
26
+export const exposeConcurrentModeAPIs = false;
27
export const warnAboutShorthandPropertyCollision = false;
28
export const enableSchedulerDebugging = false;
29
export const enableFlareAPI = false;
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
-1
@@ -21,7 +21,7 @@ export const enableProfilerTimer = __PROFILE__;
21
export const enableSchedulerTracing = __PROFILE__;
22
export const enableSuspenseServerRenderer = false;
23
export const enableSelectiveHydration = false;
24
-export const enableStableConcurrentModeAPIs = false;
24
+export const exposeConcurrentModeAPIs = false;
25
export const enableSchedulerDebugging = false;
26
export const disableJavaScriptURLs = false;
27
export const enableFlareAPI = true;
packages/shared/forks/ReactFeatureFlags.www.js
+1
-1
@@ -39,7 +39,7 @@ export const warnAboutStringRefs = false;
39
export const warnAboutDefaultPropsOnFunctionComponents = false;
40
export const disableSchedulerTimeoutBasedOnReactExpirationTime = false;
41
42
-export const enableStableConcurrentModeAPIs = false;
42
+export const exposeConcurrentModeAPIs = false;
43
44
export const enableSuspenseServerRenderer = true;
45
scripts/jest/setupEnvironment.js
+9
-1
@@ -7,7 +7,15 @@ if (NODE_ENV !== 'development' && NODE_ENV !== 'production') {
7
global.__DEV__ = NODE_ENV === 'development';
8
global.__PROFILE__ = NODE_ENV === 'development';
9
global.__UMD__ = false;
10
-global.__EXPERIMENTAL__ = process.env.RELEASE_CHANNEL === 'experimental';
10
+
11
+const RELEASE_CHANNEL = process.env.RELEASE_CHANNEL;
12
+
13
+// Default to running tests in experimental mode. If the release channel is
14
+// set via an environment variable, then check if it's "experimental".
15
+global.__EXPERIMENTAL__ =
16
+ typeof RELEASE_CHANNEL === 'string'
17
+ ? RELEASE_CHANNEL === 'experimental'
18
+ : true;
19
20
if (typeof window !== 'undefined') {
21
global.requestIdleCallback = function(callback) {
scripts/release/utils.js
+2
-2
@@ -103,8 +103,8 @@ const getBuildInfo = async () => {
103
join(cwd, 'packages', 'react', 'package.json')
104
);
105
const reactVersion = isExperimental
106
- ? `${packageJSON.version}-experimental-canary-${commit}`
107
- : `${packageJSON.version}-canary-${commit}`;
106
+ ? `${packageJSON.version}-experimental-${commit}`
107
+ : `${packageJSON.version}-${commit}`;
108
109
return {branch, buildNumber, checksum, commit, reactVersion, version};
110
};