@samitouri / QOS-React-2 / commits / 30c5daf943

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