@samitouri / QOS-React / commits / 7aa45db6a2

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