Convert refs to createRoot (#28113)
Sebastian Silbermann committed
Jan 28, 2024 at 10:41 UTC
7aa45db6a2d30e62cf5ace08e17d971c8e8b977c
2 files changed
+327
-131
packages/react-dom/src/__tests__/refs-test.js
+226
-131
@@ -10,9 +10,10 @@
10
'use strict';
11
12
let React = require('react');
13
-let ReactDOM = require('react-dom');
13
+let ReactDOMClient = require('react-dom/client');
14
let ReactFeatureFlags = require('shared/ReactFeatureFlags');
15
let ReactTestUtils = require('react-dom/test-utils');
16
+let act = require('internal-test-utils').act;
17
18
// This is testing if string refs are deleted from `instance.refs`
19
// Once support for string refs is removed, this test can be removed.
@@ -23,9 +24,10 @@ describe('reactiverefs', () => {
24
beforeEach(() => {
25
jest.resetModules();
26
React = require('react');
26
- ReactDOM = require('react-dom');
27
+ ReactDOMClient = require('react-dom/client');
28
ReactFeatureFlags = require('shared/ReactFeatureFlags');
29
ReactTestUtils = require('react-dom/test-utils');
30
+ act = require('internal-test-utils').act;
31
});
32
33
afterEach(() => {
@@ -82,7 +84,7 @@ describe('reactiverefs', () => {
84
/**
85
* Render a TestRefsComponent and ensure that the main refs are wired up.
86
*/
85
- const renderTestRefsComponent = function () {
87
+ const renderTestRefsComponent = async function () {
88
/**
89
* Only purpose is to test that refs are tracked even when applied to a
90
* component that is injected down several layers. Ref systems are difficult to
@@ -121,8 +123,17 @@ describe('reactiverefs', () => {
123
document.body.appendChild(container);
124
125
let testRefsComponent;
124
- expect(() => {
125
- testRefsComponent = ReactDOM.render(<TestRefsComponent />, container);
126
+ await expect(async () => {
127
+ const root = ReactDOMClient.createRoot(container);
128
+ await act(() => {
129
+ root.render(
130
+ <TestRefsComponent
131
+ ref={current => {
132
+ testRefsComponent = current;
133
+ }}
134
+ />,
135
+ );
136
+ });
137
}).toErrorDev([
138
'Warning: Component "div" contains the string ref "resetDiv". ' +
139
'Support for string refs will be removed in a future major release. ' +
@@ -157,8 +168,8 @@ describe('reactiverefs', () => {
168
* Ensure that for every click log there is a corresponding ref (from the
169
* perspective of the injected ClickCounter component.
170
*/
160
- it('Should increase refs with an increase in divs', () => {
161
- const testRefsComponent = renderTestRefsComponent();
171
+ it('Should increase refs with an increase in divs', async () => {
172
+ const testRefsComponent = await renderTestRefsComponent();
173
const clickIncrementer = ReactTestUtils.findRenderedDOMComponentWithClass(
174
testRefsComponent,
175
'clickIncrementer',
@@ -171,14 +182,20 @@ describe('reactiverefs', () => {
182
expectClickLogsLengthToBe(testRefsComponent, 1);
183
184
// Begin incrementing clicks (and therefore refs).
174
- clickIncrementer.click();
185
+ await act(() => {
186
+ clickIncrementer.click();
187
+ });
188
expectClickLogsLengthToBe(testRefsComponent, 2);
189
177
- clickIncrementer.click();
190
+ await act(() => {
191
+ clickIncrementer.click();
192
+ });
193
expectClickLogsLengthToBe(testRefsComponent, 3);
194
195
// Now reset again
181
- testRefsComponent.refs.resetDiv.click();
196
+ await act(() => {
197
+ testRefsComponent.refs.resetDiv.click();
198
+ });
199
expectClickLogsLengthToBe(testRefsComponent, 1);
200
});
201
});
@@ -218,9 +235,10 @@ describe('ref swapping', () => {
235
beforeEach(() => {
236
jest.resetModules();
237
React = require('react');
221
- ReactDOM = require('react-dom');
238
+ ReactDOMClient = require('react-dom/client');
239
ReactFeatureFlags = require('shared/ReactFeatureFlags');
240
ReactTestUtils = require('react-dom/test-utils');
241
+ act = require('internal-test-utils').act;
242
243
RefHopsAround = class extends React.Component {
244
state = {count: 0};
@@ -301,18 +319,23 @@ describe('ref swapping', () => {
319
expect(refHopsAround.divThreeRef.current).toEqual(thirdDiv);
320
});
321
304
- it('always has a value for this.refs', () => {
322
+ it('always has a value for this.refs', async () => {
323
class Component extends React.Component {
324
render() {
325
return <div />;
326
}
327
}
328
311
- const instance = ReactTestUtils.renderIntoDocument(<Component />);
329
+ const container = document.createElement('div');
330
+ const root = ReactDOMClient.createRoot(container);
331
+ let instance;
332
+ await act(() => {
333
+ root.render(<Component ref={current => (instance = current)} />);
334
+ });
335
expect(!!instance.refs).toBe(true);
336
});
337
315
- it('ref called correctly for stateless component', () => {
338
+ it('ref called correctly for stateless component', async () => {
339
let refCalled = 0;
340
function Inner(props) {
341
return <a ref={props.saveA} />;
@@ -332,7 +355,12 @@ describe('ref swapping', () => {
355
}
356
}
357
335
- ReactTestUtils.renderIntoDocument(<Outer />);
358
+ const container = document.createElement('div');
359
+ const root = ReactDOMClient.createRoot(container);
360
+ await act(() => {
361
+ root.render(<Outer />);
362
+ });
363
+
364
expect(refCalled).toBe(1);
365
});
366
@@ -355,58 +383,80 @@ describe('ref swapping', () => {
383
expect(a.refs[1].nodeName).toBe('DIV');
384
});
385
358
- it('provides an error for invalid refs', () => {
359
- expect(() => {
360
- ReactTestUtils.renderIntoDocument(<div ref={10} />);
361
- }).toThrow(
386
+ it('provides an error for invalid refs', async () => {
387
+ const container = document.createElement('div');
388
+ const root = ReactDOMClient.createRoot(container);
389
+ await expect(async () => {
390
+ await act(() => {
391
+ root.render(<div ref={10} />);
392
+ });
393
+ }).rejects.toThrow(
394
'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
395
);
364
- expect(() => {
365
- ReactTestUtils.renderIntoDocument(<div ref={true} />);
366
- }).toThrow(
396
+ await expect(async () => {
397
+ await act(() => {
398
+ root.render(<div ref={true} />);
399
+ });
400
+ }).rejects.toThrow(
401
'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
402
);
369
- expect(() => {
370
- ReactTestUtils.renderIntoDocument(<div ref={Symbol('foo')} />);
371
- }).toThrow(
403
+ await expect(async () => {
404
+ await act(() => {
405
+ root.render(<div ref={Symbol('foo')} />);
406
+ });
407
+ }).rejects.toThrow(
408
'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
409
);
374
- // This works
375
- ReactTestUtils.renderIntoDocument(<div ref={undefined} />);
376
- ReactTestUtils.renderIntoDocument({
377
- $$typeof: Symbol.for('react.element'),
378
- type: 'div',
379
- props: {},
380
- key: null,
381
- ref: null,
410
+
411
+ await act(() => {
412
+ root.render(<div ref={undefined} />);
413
});
383
- // But this doesn't
384
- expect(() => {
385
- ReactTestUtils.renderIntoDocument({
414
+
415
+ await act(() => {
416
+ root.render({
417
$$typeof: Symbol.for('react.element'),
418
type: 'div',
419
props: {},
420
key: null,
390
- ref: undefined,
421
+ ref: null,
422
+ });
423
+ });
424
+
425
+ // But this doesn't
426
+ await expect(async () => {
427
+ await act(() => {
428
+ root.render({
429
+ $$typeof: Symbol.for('react.element'),
430
+ type: 'div',
431
+ props: {},
432
+ key: null,
433
+ ref: undefined,
434
+ });
435
});
392
- }).toThrow(
436
+ }).rejects.toThrow(
437
'Expected ref to be a function, a string, an object returned by React.createRef(), or null.',
438
);
439
});
440
});
441
442
describe('root level refs', () => {
399
- it('attaches and detaches root refs', () => {
443
+ it('attaches and detaches root refs', async () => {
444
let inst = null;
445
446
// host node
447
let ref = jest.fn(value => (inst = value));
448
const container = document.createElement('div');
405
- let result = ReactDOM.render(<div ref={ref} />, container);
449
+ let root = ReactDOMClient.createRoot(container);
450
+ await act(() => {
451
+ root.render(<div ref={ref} />);
452
+ });
453
+ let result = container.firstChild;
454
expect(ref).toHaveBeenCalledTimes(1);
455
expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLDivElement);
456
expect(result).toBe(ref.mock.calls[0][0]);
409
- ReactDOM.unmountComponentAtNode(container);
457
+ await act(() => {
458
+ root.unmount();
459
+ });
460
expect(ref).toHaveBeenCalledTimes(2);
461
expect(ref.mock.calls[1][0]).toBe(null);
462
@@ -422,17 +472,20 @@ describe('root level refs', () => {
472
473
inst = null;
474
ref = jest.fn(value => (inst = value));
425
- result = ReactDOM.render(<Comp ref={ref} />, container);
475
+ root = ReactDOMClient.createRoot(container);
476
+ await act(() => {
477
+ root.render(<Comp ref={ref} />);
478
+ });
479
480
expect(ref).toHaveBeenCalledTimes(1);
481
expect(inst).toBeInstanceOf(Comp);
429
- expect(result).toBe(inst);
482
483
// ensure we have the correct instance
432
- expect(result.method()).toBe(true);
484
expect(inst.method()).toBe(true);
485
435
- ReactDOM.unmountComponentAtNode(container);
486
+ await act(() => {
487
+ root.unmount();
488
+ });
489
expect(ref).toHaveBeenCalledTimes(2);
490
expect(ref.mock.calls[1][0]).toBe(null);
491
@@ -441,38 +494,45 @@ describe('root level refs', () => {
494
ref = jest.fn(value => (inst = value));
495
let divInst = null;
496
const ref2 = jest.fn(value => (divInst = value));
444
- result = ReactDOM.render(
445
- [
497
+ root = ReactDOMClient.createRoot(container);
498
+ await act(() => {
499
+ root.render([
500
<Comp ref={ref} key="a" />,
501
5,
502
<div ref={ref2} key="b">
503
Hello
504
</div>,
451
- ],
452
- container,
453
- );
505
+ ]);
506
+ });
507
508
// first call should be `Comp`
509
expect(ref).toHaveBeenCalledTimes(1);
510
expect(ref.mock.calls[0][0]).toBeInstanceOf(Comp);
458
- expect(result).toBe(ref.mock.calls[0][0]);
511
512
expect(ref2).toHaveBeenCalledTimes(1);
513
expect(divInst).toBeInstanceOf(HTMLDivElement);
462
- expect(result).not.toBe(divInst);
514
464
- ReactDOM.unmountComponentAtNode(container);
515
+ await act(() => {
516
+ root.unmount();
517
+ });
518
expect(ref).toHaveBeenCalledTimes(2);
519
expect(ref.mock.calls[1][0]).toBe(null);
520
expect(ref2).toHaveBeenCalledTimes(2);
521
expect(ref2.mock.calls[1][0]).toBe(null);
522
523
// null
471
- result = ReactDOM.render(null, container);
524
+ root = ReactDOMClient.createRoot(container);
525
+ await act(() => {
526
+ root.render(null);
527
+ });
528
+ result = container.firstChild;
529
expect(result).toBe(null);
530
531
// primitives
475
- result = ReactDOM.render(5, container);
532
+ await act(() => {
533
+ root.render(5);
534
+ });
535
+ result = container.firstChild;
536
expect(result).toBeInstanceOf(Text);
537
});
538
});
@@ -489,12 +549,15 @@ describe('creating element with string ref in constructor', () => {
549
}
550
}
551
492
- it('throws an error', () => {
493
- ReactTestUtils = require('react-dom/test-utils');
552
+ it('throws an error', async () => {
553
+ await expect(async function () {
554
+ const container = document.createElement('div');
555
+ const root = ReactDOMClient.createRoot(container);
556
495
- expect(function () {
496
- ReactTestUtils.renderIntoDocument(<RefTest />);
497
- }).toThrowError(
557
+ await act(() => {
558
+ root.render(<RefTest />);
559
+ });
560
+ }).rejects.toThrowError(
561
'Element ref was specified as a string (p) but no owner was set. This could happen for one of' +
562
' the following reasons:\n' +
563
'1. You may be adding a ref to a function component\n' +
@@ -506,7 +569,7 @@ describe('creating element with string ref in constructor', () => {
569
});
570
571
describe('strings refs across renderers', () => {
509
- it('does not break', () => {
572
+ it('does not break', async () => {
573
class Parent extends React.Component {
574
render() {
575
// This component owns both refs.
@@ -524,7 +587,11 @@ describe('strings refs across renderers', () => {
587
// One ref is being rendered later using another renderer copy.
588
jest.resetModules();
589
const AnotherCopyOfReactDOM = require('react-dom');
527
- AnotherCopyOfReactDOM.render(this.props.child2, div2);
590
+ const AnotherCopyOfReactDOMClient = require('react-dom/client');
591
+ const root = AnotherCopyOfReactDOMClient.createRoot(div2);
592
+ AnotherCopyOfReactDOM.flushSync(() => {
593
+ root.render(this.props.child2);
594
+ });
595
}
596
render() {
597
// The other one is being rendered directly.
@@ -535,9 +602,20 @@ describe('strings refs across renderers', () => {
602
const div1 = document.createElement('div');
603
const div2 = document.createElement('div');
604
605
+ const root = ReactDOMClient.createRoot(div1);
606
let inst;
539
- expect(() => {
540
- inst = ReactDOM.render(<Parent />, div1);
607
+ await expect(async () => {
608
+ await act(() => {
609
+ root.render(
610
+ <Parent
611
+ ref={current => {
612
+ if (current !== null) {
613
+ inst = current;
614
+ }
615
+ }}
616
+ />,
617
+ );
618
+ });
619
}).toErrorDev([
620
'Warning: Component "Indirection" contains the string ref "child1". ' +
621
'Support for string refs will be removed in a future major release. ' +
@@ -551,9 +629,11 @@ describe('strings refs across renderers', () => {
629
expect(inst.refs.child1.tagName).toBe('DIV');
630
expect(inst.refs.child1).toBe(div1.firstChild);
631
554
- expect(() => {
632
+ await expect(async () => {
633
// Now both refs should be rendered.
556
- ReactDOM.render(<Parent />, div1);
634
+ await act(() => {
635
+ root.render(<Parent />);
636
+ });
637
}).toErrorDev(
638
[
639
'Warning: Component "Root" contains the string ref "child2". ' +
@@ -571,35 +651,41 @@ describe('strings refs across renderers', () => {
651
});
652
653
describe('refs return clean up function', () => {
574
- it('calls clean up function if it exists', () => {
654
+ it('calls clean up function if it exists', async () => {
655
const container = document.createElement('div');
656
let cleanUp = jest.fn();
657
let setup = jest.fn();
658
579
- ReactDOM.render(
580
- <div
581
- ref={_ref => {
582
- setup(_ref);
583
- return cleanUp;
584
- }}
585
- />,
586
- container,
587
- );
659
+ const root = ReactDOMClient.createRoot(container);
660
589
- ReactDOM.render(
590
- <div
591
- ref={_ref => {
592
- setup(_ref);
593
- }}
594
- />,
595
- container,
596
- );
661
+ await act(() => {
662
+ root.render(
663
+ <div
664
+ ref={_ref => {
665
+ setup(_ref);
666
+ return cleanUp;
667
+ }}
668
+ />,
669
+ );
670
+ });
671
+
672
+ await act(() => {
673
+ root.render(
674
+ <div
675
+ ref={_ref => {
676
+ setup(_ref);
677
+ }}
678
+ />,
679
+ );
680
+ });
681
682
expect(setup).toHaveBeenCalledTimes(2);
683
expect(cleanUp).toHaveBeenCalledTimes(1);
684
expect(cleanUp.mock.calls[0][0]).toBe(undefined);
685
602
- ReactDOM.render(<div ref={_ref => {}} />, container);
686
+ await act(() => {
687
+ root.render(<div ref={_ref => {}} />);
688
+ });
689
690
expect(cleanUp).toHaveBeenCalledTimes(1);
691
expect(setup).toHaveBeenCalledTimes(3);
@@ -608,34 +694,36 @@ describe('refs return clean up function', () => {
694
cleanUp = jest.fn();
695
setup = jest.fn();
696
611
- ReactDOM.render(
612
- <div
613
- ref={_ref => {
614
- setup(_ref);
615
- return cleanUp;
616
- }}
617
- />,
618
- container,
619
- );
697
+ await act(() => {
698
+ root.render(
699
+ <div
700
+ ref={_ref => {
701
+ setup(_ref);
702
+ return cleanUp;
703
+ }}
704
+ />,
705
+ );
706
+ });
707
708
expect(setup).toHaveBeenCalledTimes(1);
709
expect(cleanUp).toHaveBeenCalledTimes(0);
710
624
- ReactDOM.render(
625
- <div
626
- ref={_ref => {
627
- setup(_ref);
628
- return cleanUp;
629
- }}
630
- />,
631
- container,
632
- );
711
+ await act(() => {
712
+ root.render(
713
+ <div
714
+ ref={_ref => {
715
+ setup(_ref);
716
+ return cleanUp;
717
+ }}
718
+ />,
719
+ );
720
+ });
721
722
expect(setup).toHaveBeenCalledTimes(2);
723
expect(cleanUp).toHaveBeenCalledTimes(1);
724
});
725
638
- it('handles ref functions with stable identity', () => {
726
+ it('handles ref functions with stable identity', async () => {
727
const container = document.createElement('div');
728
const cleanUp = jest.fn();
729
const setup = jest.fn();
@@ -645,50 +733,38 @@ describe('refs return clean up function', () => {
733
return cleanUp;
734
}
735
648
- ReactDOM.render(<div ref={_onRefChange} />, container);
736
+ const root = ReactDOMClient.createRoot(container);
737
+ await act(() => {
738
+ root.render(<div ref={_onRefChange} />);
739
+ });
740
741
expect(setup).toHaveBeenCalledTimes(1);
742
expect(cleanUp).toHaveBeenCalledTimes(0);
743
653
- ReactDOM.render(
654
- <div className="niceClassName" ref={_onRefChange} />,
655
- container,
656
- );
744
+ await act(() => {
745
+ root.render(<div className="niceClassName" ref={_onRefChange} />);
746
+ });
747
748
expect(setup).toHaveBeenCalledTimes(1);
749
expect(cleanUp).toHaveBeenCalledTimes(0);
750
661
- ReactDOM.render(<div />, container);
751
+ await act(() => {
752
+ root.render(<div />);
753
+ });
754
755
expect(setup).toHaveBeenCalledTimes(1);
756
expect(cleanUp).toHaveBeenCalledTimes(1);
757
});
758
667
- it('warns if clean up function is returned when called with null', () => {
759
+ it('warns if clean up function is returned when called with null', async () => {
760
const container = document.createElement('div');
761
const cleanUp = jest.fn();
762
const setup = jest.fn();
763
let returnCleanUp = false;
764
673
- ReactDOM.render(
674
- <div
675
- ref={_ref => {
676
- setup(_ref);
677
- if (returnCleanUp) {
678
- return cleanUp;
679
- }
680
- }}
681
- />,
682
- container,
683
- );
684
-
685
- expect(setup).toHaveBeenCalledTimes(1);
686
- expect(cleanUp).toHaveBeenCalledTimes(0);
687
-
688
- returnCleanUp = true;
689
-
690
- expect(() => {
691
- ReactDOM.render(
765
+ const root = ReactDOMClient.createRoot(container);
766
+ await act(() => {
767
+ root.render(
768
<div
769
ref={_ref => {
770
setup(_ref);
@@ -697,8 +773,27 @@ describe('refs return clean up function', () => {
773
}
774
}}
775
/>,
700
- container,
776
);
777
+ });
778
+
779
+ expect(setup).toHaveBeenCalledTimes(1);
780
+ expect(cleanUp).toHaveBeenCalledTimes(0);
781
+
782
+ returnCleanUp = true;
783
+
784
+ await expect(async () => {
785
+ await act(() => {
786
+ root.render(
787
+ <div
788
+ ref={_ref => {
789
+ setup(_ref);
790
+ if (returnCleanUp) {
791
+ return cleanUp;
792
+ }
793
+ }}
794
+ />,
795
+ );
796
+ });
797
}).toErrorDev('Unexpected return value from a callback ref in div');
798
});
799
});
packages/react-dom/src/__tests__/refsLegacy-test.js
new
+101
@@ -0,0 +1,101 @@
1
+/**
2
+ * Copyright (c) Meta Platforms, Inc. and affiliates.
3
+ *
4
+ * This source code is licensed under the MIT license found in the
5
+ * LICENSE file in the root directory of this source tree.
6
+ *
7
+ * @emails react-core
8
+ */
9
+
10
+'use strict';
11
+
12
+let React = require('react');
13
+let ReactDOM = require('react-dom');
14
+
15
+describe('root level refs with legacy APIs', () => {
16
+ beforeEach(() => {
17
+ jest.resetModules();
18
+ React = require('react');
19
+ ReactDOM = require('react-dom');
20
+ });
21
+
22
+ it('attaches and detaches root refs', () => {
23
+ let inst = null;
24
+
25
+ // host node
26
+ let ref = jest.fn(value => (inst = value));
27
+ const container = document.createElement('div');
28
+ let result = ReactDOM.render(<div ref={ref} />, container);
29
+ expect(ref).toHaveBeenCalledTimes(1);
30
+ expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLDivElement);
31
+ expect(result).toBe(ref.mock.calls[0][0]);
32
+ ReactDOM.unmountComponentAtNode(container);
33
+ expect(ref).toHaveBeenCalledTimes(2);
34
+ expect(ref.mock.calls[1][0]).toBe(null);
35
+
36
+ // composite
37
+ class Comp extends React.Component {
38
+ method() {
39
+ return true;
40
+ }
41
+ render() {
42
+ return <div>Comp</div>;
43
+ }
44
+ }
45
+
46
+ inst = null;
47
+ ref = jest.fn(value => (inst = value));
48
+ result = ReactDOM.render(<Comp ref={ref} />, container);
49
+
50
+ expect(ref).toHaveBeenCalledTimes(1);
51
+ expect(inst).toBeInstanceOf(Comp);
52
+ expect(result).toBe(inst);
53
+
54
+ // ensure we have the correct instance
55
+ expect(result.method()).toBe(true);
56
+ expect(inst.method()).toBe(true);
57
+
58
+ ReactDOM.unmountComponentAtNode(container);
59
+ expect(ref).toHaveBeenCalledTimes(2);
60
+ expect(ref.mock.calls[1][0]).toBe(null);
61
+
62
+ // fragment
63
+ inst = null;
64
+ ref = jest.fn(value => (inst = value));
65
+ let divInst = null;
66
+ const ref2 = jest.fn(value => (divInst = value));
67
+ result = ReactDOM.render(
68
+ [
69
+ <Comp ref={ref} key="a" />,
70
+ 5,
71
+ <div ref={ref2} key="b">
72
+ Hello
73
+ </div>,
74
+ ],
75
+ container,
76
+ );
77
+
78
+ // first call should be `Comp`
79
+ expect(ref).toHaveBeenCalledTimes(1);
80
+ expect(ref.mock.calls[0][0]).toBeInstanceOf(Comp);
81
+ expect(result).toBe(ref.mock.calls[0][0]);
82
+
83
+ expect(ref2).toHaveBeenCalledTimes(1);
84
+ expect(divInst).toBeInstanceOf(HTMLDivElement);
85
+ expect(result).not.toBe(divInst);
86
+
87
+ ReactDOM.unmountComponentAtNode(container);
88
+ expect(ref).toHaveBeenCalledTimes(2);
89
+ expect(ref.mock.calls[1][0]).toBe(null);
90
+ expect(ref2).toHaveBeenCalledTimes(2);
91
+ expect(ref2.mock.calls[1][0]).toBe(null);
92
+
93
+ // null
94
+ result = ReactDOM.render(null, container);
95
+ expect(result).toBe(null);
96
+
97
+ // primitives
98
+ result = ReactDOM.render(5, container);
99
+ expect(result).toBeInstanceOf(Text);
100
+ });
101
+});