main
js 710 lines 19.7 KB
Raw
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;
13 let ReactDOM;
14 let ReactDOMClient;
15 let ReactDOMServer;
16 let act;
17 let assertConsoleErrorDev;
18
19 describe('ReactComponent', () => {
20 beforeEach(() => {
21 jest.resetModules();
22
23 React = require('react');
24 ReactDOM = require('react-dom');
25 ReactDOMClient = require('react-dom/client');
26 ReactDOMServer = require('react-dom/server');
27 act = require('internal-test-utils').act;
28 assertConsoleErrorDev =
29 require('internal-test-utils').assertConsoleErrorDev;
30 });
31
32 // @gate !disableLegacyMode
33 it('should throw on invalid render targets in legacy roots', () => {
34 const container = document.createElement('div');
35 // jQuery objects are basically arrays; people often pass them in by mistake
36 expect(function () {
37 ReactDOM.render(<div />, [container]);
38 }).toThrow(/Target container is not a DOM element./);
39
40 expect(function () {
41 ReactDOM.render(<div />, null);
42 }).toThrow(/Target container is not a DOM element./);
43 });
44
45 it('should throw (in dev) when children are mutated during render', async () => {
46 function Wrapper(props) {
47 props.children[1] = <p key={1} />; // Mutation is illegal
48 return <div>{props.children}</div>;
49 }
50 if (__DEV__) {
51 const container = document.createElement('div');
52 const root = ReactDOMClient.createRoot(container);
53 await expect(
54 act(() => {
55 root.render(
56 <Wrapper>
57 <span key={0} />
58 <span key={1} />
59 <span key={2} />
60 </Wrapper>,
61 );
62 }),
63 ).rejects.toThrow(/Cannot assign to read only property.*/);
64 } else {
65 const container = document.createElement('div');
66 const root = ReactDOMClient.createRoot(container);
67
68 await act(() => {
69 root.render(
70 <Wrapper>
71 <span key={0} />
72 <span key={1} />
73 <span key={2} />
74 </Wrapper>,
75 );
76 });
77 }
78 });
79
80 it('should throw (in dev) when children are mutated during update', async () => {
81 class Wrapper extends React.Component {
82 componentDidMount() {
83 this.props.children[1] = <p key={1} />; // Mutation is illegal
84 this.forceUpdate();
85 }
86
87 render() {
88 return <div>{this.props.children}</div>;
89 }
90 }
91
92 if (__DEV__) {
93 const container = document.createElement('div');
94 const root = ReactDOMClient.createRoot(container);
95 await expect(
96 act(() => {
97 root.render(
98 <Wrapper>
99 <span key={0} />
100 <span key={1} />
101 <span key={2} />
102 </Wrapper>,
103 );
104 }),
105 ).rejects.toThrow(/Cannot assign to read only property.*/);
106 } else {
107 const container = document.createElement('div');
108 const root = ReactDOMClient.createRoot(container);
109
110 await act(() => {
111 root.render(
112 <Wrapper>
113 <span key={0} />
114 <span key={1} />
115 <span key={2} />
116 </Wrapper>,
117 );
118 });
119 }
120 });
121
122 it('should not have string refs on unmounted components', async () => {
123 class Parent extends React.Component {
124 render() {
125 return (
126 <Child>
127 <div ref="test" />
128 </Child>
129 );
130 }
131
132 componentDidMount() {
133 expect(this.refs && this.refs.test).toEqual(undefined);
134 }
135 }
136
137 class Child extends React.Component {
138 render() {
139 return <div />;
140 }
141 }
142
143 const container = document.createElement('div');
144 const root = ReactDOMClient.createRoot(container);
145 await act(() => {
146 root.render(<Parent child={<span />} />);
147 });
148 });
149
150 it('should support callback-style refs', async () => {
151 const innerObj = {};
152 const outerObj = {};
153
154 class Wrapper extends React.Component {
155 getObject = () => {
156 return this.props.object;
157 };
158
159 render() {
160 return <div>{this.props.children}</div>;
161 }
162 }
163
164 let mounted = false;
165
166 class Component extends React.Component {
167 render() {
168 const inner = (
169 <Wrapper object={innerObj} ref={c => (this.innerRef = c)} />
170 );
171 const outer = (
172 <Wrapper object={outerObj} ref={c => (this.outerRef = c)}>
173 {inner}
174 </Wrapper>
175 );
176 return outer;
177 }
178
179 componentDidMount() {
180 expect(this.innerRef.getObject()).toEqual(innerObj);
181 expect(this.outerRef.getObject()).toEqual(outerObj);
182 mounted = true;
183 }
184 }
185
186 const container = document.createElement('div');
187 const root = ReactDOMClient.createRoot(container);
188 await act(() => {
189 root.render(<Component />);
190 });
191
192 expect(mounted).toBe(true);
193 });
194
195 it('should support object-style refs', async () => {
196 const innerObj = {};
197 const outerObj = {};
198
199 class Wrapper extends React.Component {
200 getObject = () => {
201 return this.props.object;
202 };
203
204 render() {
205 return <div>{this.props.children}</div>;
206 }
207 }
208
209 let mounted = false;
210
211 class Component extends React.Component {
212 constructor() {
213 super();
214 this.innerRef = React.createRef();
215 this.outerRef = React.createRef();
216 }
217 render() {
218 const inner = <Wrapper object={innerObj} ref={this.innerRef} />;
219 const outer = (
220 <Wrapper object={outerObj} ref={this.outerRef}>
221 {inner}
222 </Wrapper>
223 );
224 return outer;
225 }
226
227 componentDidMount() {
228 expect(this.innerRef.current.getObject()).toEqual(innerObj);
229 expect(this.outerRef.current.getObject()).toEqual(outerObj);
230 mounted = true;
231 }
232 }
233
234 const container = document.createElement('div');
235 const root = ReactDOMClient.createRoot(container);
236 await act(() => {
237 root.render(<Component />);
238 });
239
240 expect(mounted).toBe(true);
241 });
242
243 it('should support new-style refs with mixed-up owners', async () => {
244 class Wrapper extends React.Component {
245 getTitle = () => {
246 return this.props.title;
247 };
248
249 render() {
250 return this.props.getContent();
251 }
252 }
253
254 let mounted = false;
255
256 class Component extends React.Component {
257 getInner = () => {
258 // (With old-style refs, it's impossible to get a ref to this div
259 // because Wrapper is the current owner when this function is called.)
260 return <div className="inner" ref={c => (this.innerRef = c)} />;
261 };
262
263 render() {
264 return (
265 <Wrapper
266 title="wrapper"
267 ref={c => (this.wrapperRef = c)}
268 getContent={this.getInner}
269 />
270 );
271 }
272
273 componentDidMount() {
274 // Check .props.title to make sure we got the right elements back
275 expect(this.wrapperRef.getTitle()).toBe('wrapper');
276 expect(this.innerRef.className).toBe('inner');
277 mounted = true;
278 }
279 }
280
281 const container = document.createElement('div');
282 const root = ReactDOMClient.createRoot(container);
283
284 await act(() => {
285 root.render(<Component />);
286 });
287
288 expect(mounted).toBe(true);
289 });
290
291 it('should call refs at the correct time', async () => {
292 const log = [];
293
294 class Inner extends React.Component {
295 render() {
296 log.push(`inner ${this.props.id} render`);
297 return <div />;
298 }
299
300 componentDidMount() {
301 log.push(`inner ${this.props.id} componentDidMount`);
302 }
303
304 componentDidUpdate() {
305 log.push(`inner ${this.props.id} componentDidUpdate`);
306 }
307
308 componentWillUnmount() {
309 log.push(`inner ${this.props.id} componentWillUnmount`);
310 }
311 }
312
313 class Outer extends React.Component {
314 render() {
315 return (
316 <div>
317 <Inner
318 id={1}
319 ref={c => {
320 log.push(`ref 1 got ${c ? `instance ${c.props.id}` : 'null'}`);
321 }}
322 />
323 <Inner
324 id={2}
325 ref={c => {
326 log.push(`ref 2 got ${c ? `instance ${c.props.id}` : 'null'}`);
327 }}
328 />
329 </div>
330 );
331 }
332
333 componentDidMount() {
334 log.push('outer componentDidMount');
335 }
336
337 componentDidUpdate() {
338 log.push('outer componentDidUpdate');
339 }
340
341 componentWillUnmount() {
342 log.push('outer componentWillUnmount');
343 }
344 }
345
346 // mount, update, unmount
347 const el = document.createElement('div');
348 log.push('start mount');
349 const root = ReactDOMClient.createRoot(el);
350 await act(() => {
351 root.render(<Outer />);
352 });
353 log.push('start update');
354 await act(() => {
355 root.render(<Outer />);
356 });
357 log.push('start unmount');
358 await act(() => {
359 root.unmount();
360 });
361
362 expect(log).toEqual([
363 'start mount',
364 'inner 1 render',
365 'inner 2 render',
366 'inner 1 componentDidMount',
367 'ref 1 got instance 1',
368 'inner 2 componentDidMount',
369 'ref 2 got instance 2',
370 'outer componentDidMount',
371 'start update',
372 // Previous (equivalent) refs get cleared
373 // Fiber renders first, resets refs later
374 'inner 1 render',
375 'inner 2 render',
376 'ref 1 got null',
377 'ref 2 got null',
378 'inner 1 componentDidUpdate',
379 'ref 1 got instance 1',
380 'inner 2 componentDidUpdate',
381 'ref 2 got instance 2',
382 'outer componentDidUpdate',
383 'start unmount',
384 'outer componentWillUnmount',
385 'ref 1 got null',
386 'inner 1 componentWillUnmount',
387 'ref 2 got null',
388 'inner 2 componentWillUnmount',
389 ]);
390 });
391
392 // @gate !disableLegacyMode
393 it('fires the callback after a component is rendered in legacy roots', () => {
394 const callback = jest.fn();
395 const container = document.createElement('div');
396 ReactDOM.render(<div />, container, callback);
397 expect(callback).toHaveBeenCalledTimes(1);
398 ReactDOM.render(<div className="foo" />, container, callback);
399 expect(callback).toHaveBeenCalledTimes(2);
400 ReactDOM.render(<span />, container, callback);
401 expect(callback).toHaveBeenCalledTimes(3);
402 });
403
404 it('throws usefully when rendering badly-typed elements', async () => {
405 const container = document.createElement('div');
406 const root = ReactDOMClient.createRoot(container);
407
408 const X = undefined;
409 const XElement = <X />;
410 await expect(async () => {
411 await act(() => {
412 root.render(XElement);
413 });
414 }).rejects.toThrow(
415 'Element type is invalid: expected a string (for built-in components) ' +
416 'or a class/function (for composite components) but got: undefined.' +
417 (__DEV__
418 ? " You likely forgot to export your component from the file it's " +
419 'defined in, or you might have mixed up default and named imports.'
420 : ''),
421 );
422
423 const Y = null;
424 const YElement = <Y />;
425 await expect(async () => {
426 await act(() => {
427 root.render(YElement);
428 });
429 }).rejects.toThrow(
430 'Element type is invalid: expected a string (for built-in components) ' +
431 'or a class/function (for composite components) but got: null.',
432 );
433
434 const Z = true;
435 const ZElement = <Z />;
436 await expect(async () => {
437 await act(() => {
438 root.render(ZElement);
439 });
440 }).rejects.toThrow(
441 'Element type is invalid: expected a string (for built-in components) ' +
442 'or a class/function (for composite components) but got: boolean.',
443 );
444 });
445
446 it('includes owner name in the error about badly-typed elements', async () => {
447 const X = undefined;
448
449 function Indirection(props) {
450 return <div>{props.children}</div>;
451 }
452
453 function Bar() {
454 return (
455 <Indirection>
456 <X />
457 </Indirection>
458 );
459 }
460
461 function Foo() {
462 return <Bar />;
463 }
464
465 const container = document.createElement('div');
466 const root = ReactDOMClient.createRoot(container);
467 await expect(async () => {
468 await act(() => {
469 root.render(<Foo />);
470 });
471 }).rejects.toThrow(
472 'Element type is invalid: expected a string (for built-in components) ' +
473 'or a class/function (for composite components) but got: undefined.' +
474 (__DEV__
475 ? " You likely forgot to export your component from the file it's " +
476 'defined in, or you might have mixed up default and named imports.' +
477 '\n\nCheck the render method of `Bar`.'
478 : ''),
479 );
480 });
481
482 it('throws if a plain object is used as a child', async () => {
483 const children = {
484 x: <span />,
485 y: <span />,
486 z: <span />,
487 };
488 const element = <div>{[children]}</div>;
489 const container = document.createElement('div');
490 const root = ReactDOMClient.createRoot(container);
491 await expect(
492 act(() => {
493 root.render(element);
494 }),
495 ).rejects.toThrow(
496 'Objects are not valid as a React child (found: object with keys {x, y, z}). ' +
497 'If you meant to render a collection of children, use an array instead.',
498 );
499 });
500
501 it('throws if a legacy element is used as a child', async () => {
502 const inlinedElement = {
503 $$typeof: Symbol.for('react.element'),
504 type: 'div',
505 key: null,
506 ref: null,
507 props: {},
508 _owner: null,
509 };
510 const element = <div>{[inlinedElement]}</div>;
511 const container = document.createElement('div');
512 const root = ReactDOMClient.createRoot(container);
513 await expect(
514 act(() => {
515 root.render(element);
516 }),
517 ).rejects.toThrow(
518 'A React Element from an older version of React was rendered. ' +
519 'This is not supported. It can happen if:\n' +
520 '- Multiple copies of the "react" package is used.\n' +
521 '- A library pre-bundled an old copy of "react" or "react/jsx-runtime".\n' +
522 '- A compiler tries to "inline" JSX instead of using the runtime.',
523 );
524 });
525
526 it('throws if a plain object even if it is in an owner', async () => {
527 class Foo extends React.Component {
528 render() {
529 const children = {
530 a: <span />,
531 b: <span />,
532 c: <span />,
533 };
534 return <div>{[children]}</div>;
535 }
536 }
537 const container = document.createElement('div');
538 const root = ReactDOMClient.createRoot(container);
539 await expect(
540 act(() => {
541 root.render(<Foo />);
542 }),
543 ).rejects.toThrow(
544 'Objects are not valid as a React child (found: object with keys {a, b, c}).' +
545 ' If you meant to render a collection of children, use an array ' +
546 'instead.',
547 );
548 });
549
550 it('throws if a plain object is used as a child when using SSR', async () => {
551 const children = {
552 x: <span />,
553 y: <span />,
554 z: <span />,
555 };
556 const element = <div>{[children]}</div>;
557 expect(() => {
558 ReactDOMServer.renderToString(element);
559 }).toThrow(
560 'Objects are not valid as a React child (found: object with keys {x, y, z}). ' +
561 'If you meant to render a collection of children, use ' +
562 'an array instead.',
563 );
564 });
565
566 it('throws if a plain object even if it is in an owner when using SSR', async () => {
567 class Foo extends React.Component {
568 render() {
569 const children = {
570 a: <span />,
571 b: <span />,
572 c: <span />,
573 };
574 return <div>{[children]}</div>;
575 }
576 }
577 const container = document.createElement('div');
578 expect(() => {
579 ReactDOMServer.renderToString(<Foo />, container);
580 }).toThrow(
581 'Objects are not valid as a React child (found: object with keys {a, b, c}). ' +
582 'If you meant to render a collection of children, use ' +
583 'an array instead.',
584 );
585 });
586
587 describe('with new features', () => {
588 it('warns on function as a return value from a function', async () => {
589 function Foo() {
590 return Foo;
591 }
592 const container = document.createElement('div');
593 const root = ReactDOMClient.createRoot(container);
594 await act(() => {
595 root.render(<Foo />);
596 });
597 assertConsoleErrorDev([
598 'Functions are not valid as a React child. This may happen if ' +
599 'you return Foo instead of <Foo /> from render. ' +
600 'Or maybe you meant to call this function rather than return it.\n' +
601 ' <Foo>{Foo}</Foo>\n' +
602 ' in Foo (at **)',
603 ]);
604 });
605
606 it('warns on function as a return value from a class', async () => {
607 class Foo extends React.Component {
608 render() {
609 return Foo;
610 }
611 }
612 const container = document.createElement('div');
613 const root = ReactDOMClient.createRoot(container);
614
615 await act(() => {
616 root.render(<Foo />);
617 });
618 assertConsoleErrorDev([
619 'Functions are not valid as a React child. This may happen if ' +
620 'you return Foo instead of <Foo /> from render. ' +
621 'Or maybe you meant to call this function rather than return it.\n' +
622 ' <Foo>{Foo}</Foo>\n' +
623 ' in Foo (at **)',
624 ]);
625 });
626
627 it('warns on function as a child to host component', async () => {
628 function Foo() {
629 return (
630 <div>
631 <span>{Foo}</span>
632 </div>
633 );
634 }
635 const container = document.createElement('div');
636 const root = ReactDOMClient.createRoot(container);
637 await act(() => {
638 root.render(<Foo />);
639 });
640 assertConsoleErrorDev([
641 'Functions are not valid as a React child. This may happen if ' +
642 'you return Foo instead of <Foo /> from render. ' +
643 'Or maybe you meant to call this function rather than return it.\n' +
644 ' <span>{Foo}</span>\n' +
645 ' in span (at **)\n' +
646 ' in Foo (at **)',
647 ]);
648 });
649
650 it('does not warn for function-as-a-child that gets resolved', async () => {
651 function Bar(props) {
652 return props.children();
653 }
654 function Foo() {
655 return <Bar>{() => 'Hello'}</Bar>;
656 }
657 const container = document.createElement('div');
658 const root = ReactDOMClient.createRoot(container);
659 await act(() => {
660 root.render(<Foo />);
661 });
662
663 expect(container.innerHTML).toBe('Hello');
664 });
665
666 it('deduplicates function type warnings based on component type', async () => {
667 class Foo extends React.PureComponent {
668 constructor() {
669 super();
670 this.state = {type: 'mushrooms'};
671 }
672 render() {
673 return (
674 <div>
675 {Foo}
676 {Foo}
677 <span>
678 {Foo}
679 {Foo}
680 </span>
681 </div>
682 );
683 }
684 }
685 const container = document.createElement('div');
686 const root = ReactDOMClient.createRoot(container);
687 let component;
688 await act(() => {
689 root.render(<Foo ref={current => (component = current)} />);
690 });
691 assertConsoleErrorDev([
692 'Functions are not valid as a React child. This may happen if ' +
693 'you return Foo instead of <Foo /> from render. ' +
694 'Or maybe you meant to call this function rather than return it.\n' +
695 ' <div>{Foo}</div>\n' +
696 ' in div (at **)\n' +
697 ' in Foo (at **)',
698 'Functions are not valid as a React child. This may happen if ' +
699 'you return Foo instead of <Foo /> from render. ' +
700 'Or maybe you meant to call this function rather than return it.\n' +
701 ' <span>{Foo}</span>\n' +
702 ' in span (at **)\n' +
703 ' in Foo (at **)',
704 ]);
705 await act(() => {
706 component.setState({type: 'portobello mushrooms'});
707 });
708 });
709 });
710 });