@samitouri / QOS-React / commits / 4c73da8cbd

Convert ReactCompositeComponent to createRoot (#28099)

Moves tests depending on legacy APIs to `ReactLegacyCompositeComponents` and updates the rest.

Ricky committed Jan 29, 2024 at 14:03 UTC 4c73da8cbdbd2493827f86ed1991c3770ecb9625
2 files changed +1397 -1006
packages/react-dom/src/__tests__/ReactCompositeComponent-test.js
+597 -1006
@@ -13,9 +13,11 @@ let ChildUpdates;
13 let MorphingComponent;
14 let React;
15 let ReactDOM;
16 +let ReactDOMClient;
17 let ReactCurrentOwner;
17 -let ReactTestUtils;
18 -let PropTypes;
18 +let Scheduler;
19 +let assertLog;
20 +let act;
21
22 describe('ReactCompositeComponent', () => {
23 const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -64,55 +66,153 @@ describe('ReactCompositeComponent', () => {
66 jest.resetModules();
67 React = require('react');
68 ReactDOM = require('react-dom');
69 + ReactDOMClient = require('react-dom/client');
70 ReactCurrentOwner =
71 require('react').__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
72 .ReactCurrentOwner;
70 - ReactTestUtils = require('react-dom/test-utils');
71 - PropTypes = require('prop-types');
73 + Scheduler = require('scheduler');
74 + assertLog = require('internal-test-utils').assertLog;
75 + act = require('internal-test-utils').act;
76 + });
77 +
78 + describe('MorphingComponent', () => {
79 + let instance;
80 + let childInstance;
81
73 - MorphingComponent = class extends React.Component {
74 - state = {activated: false};
82 + beforeEach(() => {
83 + MorphingComponent = class extends React.Component {
84 + state = {activated: false};
85 + xRef = React.createRef();
86
76 - xRef = React.createRef();
87 + componentDidMount() {
88 + instance = this;
89 + }
90 +
91 + _toggleActivatedState = () => {
92 + this.setState({activated: !this.state.activated});
93 + };
94
78 - _toggleActivatedState = () => {
79 - this.setState({activated: !this.state.activated});
95 + render() {
96 + const toggleActivatedState = this._toggleActivatedState;
97 + return !this.state.activated ? (
98 + <a ref={this.xRef} onClick={toggleActivatedState} />
99 + ) : (
100 + <b ref={this.xRef} onClick={toggleActivatedState} />
101 + );
102 + }
103 };
104
82 - render() {
83 - const toggleActivatedState = this._toggleActivatedState;
84 - return !this.state.activated ? (
85 - <a ref={this.xRef} onClick={toggleActivatedState} />
86 - ) : (
87 - <b ref={this.xRef} onClick={toggleActivatedState} />
88 - );
89 - }
90 - };
105 + /**
106 + * We'll use this to ensure that an old version is not cached when it is
107 + * reallocated again.
108 + */
109 + ChildUpdates = class extends React.Component {
110 + anchorRef = React.createRef();
111
92 - /**
93 - * We'll use this to ensure that an old version is not cached when it is
94 - * reallocated again.
95 - */
96 - ChildUpdates = class extends React.Component {
97 - anchorRef = React.createRef();
112 + componentDidMount() {
113 + childInstance = this;
114 + }
115 +
116 + getAnchor = () => {
117 + return this.anchorRef.current;
118 + };
119
99 - getAnchor = () => {
100 - return this.anchorRef.current;
120 + render() {
121 + const className = this.props.anchorClassOn ? 'anchorClass' : '';
122 + return this.props.renderAnchor ? (
123 + <a ref={this.anchorRef} className={className} />
124 + ) : (
125 + <b />
126 + );
127 + }
128 };
129 + });
130 + it('should support rendering to different child types over time', async () => {
131 + const root = ReactDOMClient.createRoot(document.createElement('div'));
132 + await act(() => {
133 + root.render(<MorphingComponent />);
134 + });
135 + expect(instance.xRef.current.tagName).toBe('A');
136 +
137 + await act(() => {
138 + instance._toggleActivatedState();
139 + });
140 + expect(instance.xRef.current.tagName).toBe('B');
141 +
142 + await act(() => {
143 + instance._toggleActivatedState();
144 + });
145 + expect(instance.xRef.current.tagName).toBe('A');
146 + });
147
103 - render() {
104 - const className = this.props.anchorClassOn ? 'anchorClass' : '';
105 - return this.props.renderAnchor ? (
106 - <a ref={this.anchorRef} className={className} />
107 - ) : (
108 - <b />
109 - );
148 + it('should react to state changes from callbacks', async () => {
149 + const container = document.createElement('div');
150 + document.body.appendChild(container);
151 + const root = ReactDOMClient.createRoot(container);
152 + try {
153 + await act(() => {
154 + root.render(<MorphingComponent />);
155 + });
156 + expect(instance.xRef.current.tagName).toBe('A');
157 + await act(() => {
158 + instance.xRef.current.click();
159 + });
160 + expect(instance.xRef.current.tagName).toBe('B');
161 + } finally {
162 + document.body.removeChild(container);
163 + root.unmount();
164 }
111 - };
165 + });
166 +
167 + it('should rewire refs when rendering to different child types', async () => {
168 + const container = document.createElement('div');
169 + const root = ReactDOMClient.createRoot(container);
170 + await act(() => {
171 + root.render(<MorphingComponent />);
172 + });
173 + expect(instance.xRef.current.tagName).toBe('A');
174 +
175 + await act(() => {
176 + instance._toggleActivatedState();
177 + });
178 + expect(instance.xRef.current.tagName).toBe('B');
179 +
180 + await act(() => {
181 + instance._toggleActivatedState();
182 + });
183 + expect(instance.xRef.current.tagName).toBe('A');
184 + });
185 +
186 + it('should not cache old DOM nodes when switching constructors', async () => {
187 + const container = document.createElement('div');
188 + const root = ReactDOMClient.createRoot(container);
189 + await act(() => {
190 + root.render(<ChildUpdates renderAnchor={true} anchorClassOn={false} />);
191 + });
192 + await act(() => {
193 + root.render(
194 + // Warm any cache
195 + <ChildUpdates renderAnchor={true} anchorClassOn={true} />,
196 + );
197 + });
198 + await act(() => {
199 + root.render(
200 + // Clear out the anchor
201 + <ChildUpdates renderAnchor={false} anchorClassOn={true} />,
202 + );
203 + });
204 + await act(() => {
205 + root.render(
206 + // rerender
207 + <ChildUpdates renderAnchor={true} anchorClassOn={false} />,
208 + );
209 + });
210 + expect(childInstance.getAnchor().className).toBe('');
211 + });
212 });
213
214 if (require('shared/ReactFeatureFlags').disableModulePatternComponents) {
115 - it('should not support module pattern components', () => {
215 + it('should not support module pattern components', async () => {
216 function Child({test}) {
217 return {
218 render() {
@@ -122,8 +222,13 @@ describe('ReactCompositeComponent', () => {
222 }
223
224 const el = document.createElement('div');
225 + const root = ReactDOMClient.createRoot(el);
226 expect(() => {
126 - expect(() => ReactDOM.render(<Child test="test" />, el)).toThrow(
227 + expect(() => {
228 + ReactDOM.flushSync(() => {
229 + root.render(<Child test="test" />);
230 + });
231 + }).toThrow(
232 'Objects are not valid as a React child (found: object with keys {render}).',
233 );
234 }).toErrorDev(
@@ -147,7 +252,12 @@ describe('ReactCompositeComponent', () => {
252 }
253
254 const el = document.createElement('div');
150 - expect(() => ReactDOM.render(<Child test="test" />, el)).toErrorDev(
255 + const root = ReactDOMClient.createRoot(el);
256 + expect(() => {
257 + ReactDOM.flushSync(() => {
258 + root.render(<Child test="test" />);
259 + });
260 + }).toErrorDev(
261 'Warning: The <Child /> component appears to be a function component that returns a class instance. ' +
262 'Change Child to a class that extends React.Component instead. ' +
263 "If you can't use a class try assigning the prototype on the function as a workaround. " +
@@ -159,70 +269,7 @@ describe('ReactCompositeComponent', () => {
269 });
270 }
271
162 - it('should support rendering to different child types over time', () => {
163 - const instance = ReactTestUtils.renderIntoDocument(<MorphingComponent />);
164 - let el = ReactDOM.findDOMNode(instance);
165 - expect(el.tagName).toBe('A');
166 -
167 - instance._toggleActivatedState();
168 - el = ReactDOM.findDOMNode(instance);
169 - expect(el.tagName).toBe('B');
170 -
171 - instance._toggleActivatedState();
172 - el = ReactDOM.findDOMNode(instance);
173 - expect(el.tagName).toBe('A');
174 - });
175 -
176 - it('should react to state changes from callbacks', () => {
177 - const container = document.createElement('div');
178 - document.body.appendChild(container);
179 - try {
180 - const instance = ReactDOM.render(<MorphingComponent />, container);
181 - let el = ReactDOM.findDOMNode(instance);
182 - expect(el.tagName).toBe('A');
183 - el.click();
184 - el = ReactDOM.findDOMNode(instance);
185 - expect(el.tagName).toBe('B');
186 - } finally {
187 - document.body.removeChild(container);
188 - }
189 - });
190 -
191 - it('should rewire refs when rendering to different child types', () => {
192 - const instance = ReactTestUtils.renderIntoDocument(<MorphingComponent />);
193 -
194 - expect(instance.xRef.current.tagName).toBe('A');
195 - instance._toggleActivatedState();
196 - expect(instance.xRef.current.tagName).toBe('B');
197 - instance._toggleActivatedState();
198 - expect(instance.xRef.current.tagName).toBe('A');
199 - });
200 -
201 - it('should not cache old DOM nodes when switching constructors', () => {
202 - const container = document.createElement('div');
203 - const instance = ReactDOM.render(
204 - <ChildUpdates renderAnchor={true} anchorClassOn={false} />,
205 - container,
206 - );
207 - ReactDOM.render(
208 - // Warm any cache
209 - <ChildUpdates renderAnchor={true} anchorClassOn={true} />,
210 - container,
211 - );
212 - ReactDOM.render(
213 - // Clear out the anchor
214 - <ChildUpdates renderAnchor={false} anchorClassOn={true} />,
215 - container,
216 - );
217 - ReactDOM.render(
218 - // rerender
219 - <ChildUpdates renderAnchor={true} anchorClassOn={false} />,
220 - container,
221 - );
222 - expect(instance.getAnchor().className).toBe('');
223 - });
224 -
225 - it('should use default values for undefined props', () => {
272 + it('should use default values for undefined props', async () => {
273 class Component extends React.Component {
274 static defaultProps = {prop: 'testKey'};
275
@@ -231,21 +278,29 @@ describe('ReactCompositeComponent', () => {
278 }
279 }
280
234 - const instance1 = ReactTestUtils.renderIntoDocument(<Component />);
281 + let instance1;
282 + let instance2;
283 + let instance3;
284 + const root = ReactDOMClient.createRoot(document.createElement('div'));
285 + await act(() => {
286 + root.render(<Component ref={ref => (instance1 = ref)} />);
287 + });
288 expect(instance1.props).toEqual({prop: 'testKey'});
289
237 - const instance2 = ReactTestUtils.renderIntoDocument(
238 - <Component prop={undefined} />,
239 - );
290 + await act(() => {
291 + root.render(
292 + <Component ref={ref => (instance2 = ref)} prop={undefined} />,
293 + );
294 + });
295 expect(instance2.props).toEqual({prop: 'testKey'});
296
242 - const instance3 = ReactTestUtils.renderIntoDocument(
243 - <Component prop={null} />,
244 - );
297 + await act(() => {
298 + root.render(<Component ref={ref => (instance3 = ref)} prop={null} />);
299 + });
300 expect(instance3.props).toEqual({prop: null});
301 });
302
248 - it('should not mutate passed-in props object', () => {
303 + it('should not mutate passed-in props object', async () => {
304 class Component extends React.Component {
305 static defaultProps = {prop: 'testKey'};
306
@@ -255,8 +310,11 @@ describe('ReactCompositeComponent', () => {
310 }
311
312 const inputProps = {};
258 - let instance1 = <Component {...inputProps} />;
259 - instance1 = ReactTestUtils.renderIntoDocument(instance1);
313 + let instance1;
314 + const root = ReactDOMClient.createRoot(document.createElement('div'));
315 + await act(() => {
316 + root.render(<Component {...inputProps} ref={ref => (instance1 = ref)} />);
317 + });
318 expect(instance1.props.prop).toBe('testKey');
319
320 // We don't mutate the input, just in case the caller wants to do something
@@ -264,19 +322,24 @@ describe('ReactCompositeComponent', () => {
322 expect(inputProps.prop).not.toBeDefined();
323 });
324
267 - it('should warn about `forceUpdate` on not-yet-mounted components', () => {
325 + it('should warn about `forceUpdate` on not-yet-mounted components', async () => {
326 class MyComponent extends React.Component {
327 constructor(props) {
328 super(props);
329 this.forceUpdate();
330 }
331 render() {
274 - return <div />;
332 + return <div>foo</div>;
333 }
334 }
335
336 const container = document.createElement('div');
279 - expect(() => ReactDOM.render(<MyComponent />, container)).toErrorDev(
337 + const root = ReactDOMClient.createRoot(container);
338 + expect(() => {
339 + ReactDOM.flushSync(() => {
340 + root.render(<MyComponent />);
341 + });
342 + }).toErrorDev(
343 "Warning: Can't call forceUpdate on a component that is not yet mounted. " +
344 'This is a no-op, but it might indicate a bug in your application. ' +
345 'Instead, assign to `this.state` directly or define a `state = {};` ' +
@@ -285,22 +348,32 @@ describe('ReactCompositeComponent', () => {
348
349 // No additional warning should be recorded
350 const container2 = document.createElement('div');
288 - ReactDOM.render(<MyComponent />, container2);
351 + const root2 = ReactDOMClient.createRoot(container2);
352 + await act(() => {
353 + root2.render(<MyComponent />);
354 + });
355 + expect(container2.firstChild.textContent).toBe('foo');
356 });
357
291 - it('should warn about `setState` on not-yet-mounted components', () => {
358 + it('should warn about `setState` on not-yet-mounted components', async () => {
359 class MyComponent extends React.Component {
360 constructor(props) {
361 super(props);
362 this.setState();
363 }
364 render() {
298 - return <div />;
365 + return <div>foo</div>;
366 }
367 }
368
369 const container = document.createElement('div');
303 - expect(() => ReactDOM.render(<MyComponent />, container)).toErrorDev(
370 + const root = ReactDOMClient.createRoot(container);
371 +
372 + expect(() => {
373 + ReactDOM.flushSync(() => {
374 + root.render(<MyComponent />);
375 + });
376 + }).toErrorDev(
377 "Warning: Can't call setState on a component that is not yet mounted. " +
378 'This is a no-op, but it might indicate a bug in your application. ' +
379 'Instead, assign to `this.state` directly or define a `state = {};` ' +
@@ -309,67 +382,87 @@ describe('ReactCompositeComponent', () => {
382
383 // No additional warning should be recorded
384 const container2 = document.createElement('div');
312 - ReactDOM.render(<MyComponent />, container2);
385 + const root2 = ReactDOMClient.createRoot(container2);
386 + await act(() => {
387 + root2.render(<MyComponent />);
388 + });
389 + expect(container2.firstChild.textContent).toBe('foo');
390 });
391
315 - it('should not warn about `forceUpdate` on unmounted components', () => {
392 + it('should not warn about `forceUpdate` on unmounted components', async () => {
393 const container = document.createElement('div');
394 document.body.appendChild(container);
395
396 + let instance;
397 class Component extends React.Component {
398 + componentDidMount() {
399 + instance = this;
400 + }
401 +
402 render() {
403 return <div />;
404 }
405 }
406
325 - let instance = <Component />;
326 - expect(instance.forceUpdate).not.toBeDefined();
407 + const component = <Component />;
408 + expect(component.forceUpdate).not.toBeDefined();
409 + const root = ReactDOMClient.createRoot(container);
410 + await act(() => {
411 + root.render(component);
412 + });
413
328 - instance = ReactDOM.render(instance, container);
414 instance.forceUpdate();
415
331 - ReactDOM.unmountComponentAtNode(container);
416 + root.unmount(container);
417
418 instance.forceUpdate();
419 instance.forceUpdate();
420 });
421
337 - it('should not warn about `setState` on unmounted components', () => {
422 + it('should not warn about `setState` on unmounted components', async () => {
423 const container = document.createElement('div');
424 document.body.appendChild(container);
425
341 - let renders = 0;
342 -
426 class Component extends React.Component {
427 state = {value: 0};
428
429 render() {
347 - renders++;
430 + Scheduler.log('render ' + this.state.value);
431 return <div />;
432 }
433 }
434
352 - let instance;
353 - ReactDOM.render(
354 - <div>
355 - <span>
356 - <Component ref={c => (instance = c || instance)} />
357 - </span>
358 - </div>,
359 - container,
360 - );
435 + let ref;
436 + const root = ReactDOMClient.createRoot(container);
437 + await act(() => {
438 + root.render(
439 + <div>
440 + <span>
441 + <Component ref={c => (ref = c || ref)} />
442 + </span>
443 + </div>,
444 + );
445 + });
446
362 - expect(renders).toBe(1);
447 + assertLog(['render 0']);
448
364 - instance.setState({value: 1});
365 - expect(renders).toBe(2);
449 + await act(() => {
450 + ref.setState({value: 1});
451 + });
452 + assertLog(['render 1']);
453
367 - ReactDOM.render(<div />, container);
368 - instance.setState({value: 2});
369 - expect(renders).toBe(2);
454 + await act(() => {
455 + root.render(<div />);
456 + });
457 +
458 + await act(() => {
459 + ref.setState({value: 2});
460 + });
461 + // setState on an unmounted component is a noop.
462 + assertLog([]);
463 });
464
372 - it('should silently allow `setState`, not call cb on unmounting components', () => {
465 + it('should silently allow `setState`, not call cb on unmounting components', async () => {
466 let cbCalled = false;
467 const container = document.createElement('div');
468 document.body.appendChild(container);
@@ -389,24 +482,33 @@ describe('ReactCompositeComponent', () => {
482 return <div />;
483 }
484 }
392 -
393 - const instance = ReactDOM.render(<Component />, container);
485 + let instance;
486 + const root = ReactDOMClient.createRoot(container);
487 + await act(() => {
488 + root.render(<Component ref={c => (instance = c)} />);
489 + });
490 + await act(() => {
491 + instance.setState({value: 1});
492 + });
493 instance.setState({value: 1});
494
396 - ReactDOM.unmountComponentAtNode(container);
495 + root.unmount();
496 expect(cbCalled).toBe(false);
497 });
498
400 - it('should warn when rendering a class with a render method that does not extend React.Component', () => {
499 + it('should warn when rendering a class with a render method that does not extend React.Component', async () => {
500 const container = document.createElement('div');
501 class ClassWithRenderNotExtended {
502 render() {
503 return <div />;
504 }
505 }
506 + const root = ReactDOMClient.createRoot(container);
507 expect(() => {
508 expect(() => {
409 - ReactDOM.render(<ClassWithRenderNotExtended />, container);
509 + ReactDOM.flushSync(() => {
510 + root.render(<ClassWithRenderNotExtended />);
511 + });
512 }).toThrow(TypeError);
513 }).toErrorDev(
514 'Warning: The <ClassWithRenderNotExtended /> component appears to have a render method, ' +
@@ -416,33 +518,33 @@ describe('ReactCompositeComponent', () => {
518
519 // Test deduplication
520 expect(() => {
419 - ReactDOM.render(<ClassWithRenderNotExtended />, container);
521 + ReactDOM.flushSync(() => {
522 + root.render(<ClassWithRenderNotExtended />);
523 + });
524 }).toThrow(TypeError);
525 });
526
423 - it('should warn about `setState` in render', () => {
527 + it('should warn about `setState` in render', async () => {
528 const container = document.createElement('div');
529
426 - let renderedState = -1;
427 - let renderPasses = 0;
428 -
530 class Component extends React.Component {
531 state = {value: 0};
532
533 render() {
433 - renderPasses++;
434 - renderedState = this.state.value;
534 + Scheduler.log('render ' + this.state.value);
535 if (this.state.value === 0) {
536 this.setState({value: 1});
537 }
438 - return <div />;
538 + return <div>foo {this.state.value}</div>;
539 }
540 }
541
542 let instance;
443 -
543 + const root = ReactDOMClient.createRoot(container);
544 expect(() => {
445 - instance = ReactDOM.render(<Component />, container);
545 + ReactDOM.flushSync(() => {
546 + root.render(<Component ref={ref => (instance = ref)} />);
547 + });
548 }).toErrorDev(
549 'Cannot update during an existing state transition (such as within ' +
550 '`render`). Render methods should be a pure function of props and state.',
@@ -451,40 +553,37 @@ describe('ReactCompositeComponent', () => {
553 // The setState call is queued and then executed as a second pass. This
554 // behavior is undefined though so we're free to change it to suit the
555 // implementation details.
454 - expect(renderPasses).toBe(2);
455 - expect(renderedState).toBe(1);
556 + assertLog(['render 0', 'render 1']);
557 expect(instance.state.value).toBe(1);
558
559 // Forcing a rerender anywhere will cause the update to happen.
459 - const instance2 = ReactDOM.render(<Component prop={123} />, container);
460 - expect(instance).toBe(instance2);
461 - expect(renderedState).toBe(1);
462 - expect(instance2.state.value).toBe(1);
463 -
464 - // Test deduplication; (no additional warnings are expected).
465 - ReactDOM.unmountComponentAtNode(container);
466 - ReactDOM.render(<Component prop={123} />, container);
560 + await act(() => {
561 + root.render(<Component prop={123} />);
562 + });
563 + assertLog(['render 1']);
564 });
565
469 - it('should cleanup even if render() fatals', () => {
566 + it('should cleanup even if render() fatals', async () => {
567 class BadComponent extends React.Component {
568 render() {
569 throw new Error();
570 }
571 }
572
476 - let instance = <BadComponent />;
477 -
573 + const instance = <BadComponent />;
574 expect(ReactCurrentOwner.current).toBe(null);
575
576 + const root = ReactDOMClient.createRoot(document.createElement('div'));
577 expect(() => {
481 - instance = ReactTestUtils.renderIntoDocument(instance);
578 + ReactDOM.flushSync(() => {
579 + root.render(instance);
580 + });
581 }).toThrow();
582
583 expect(ReactCurrentOwner.current).toBe(null);
584 });
585
487 - it('should call componentWillUnmount before unmounting', () => {
586 + it('should call componentWillUnmount before unmounting', async () => {
587 const container = document.createElement('div');
588 let innerUnmounted = false;
589
@@ -500,572 +599,149 @@ describe('ReactCompositeComponent', () => {
599 }
600
601 class Inner extends React.Component {
503 - componentWillUnmount() {
504 - innerUnmounted = true;
505 - }
506 -
507 - render() {
508 - return <div />;
509 - }
510 - }
511 -
512 - ReactDOM.render(<Component />, container);
513 - ReactDOM.unmountComponentAtNode(container);
514 - expect(innerUnmounted).toBe(true);
515 - });
516 -
517 - it('should warn when shouldComponentUpdate() returns undefined', () => {
518 - class ClassComponent extends React.Component {
519 - state = {bogus: false};
520 -
521 - shouldComponentUpdate() {
522 - return undefined;
523 - }
524 -
525 - render() {
526 - return <div />;
527 - }
528 - }
529 -
530 - const instance = ReactTestUtils.renderIntoDocument(<ClassComponent />);
531 -
532 - expect(() => instance.setState({bogus: true})).toErrorDev(
533 - 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' +
534 - 'boolean value. Make sure to return true or false.',
535 - );
536 - });
537 -
538 - it('should warn when componentDidUnmount method is defined', () => {
539 - class Component extends React.Component {
540 - componentDidUnmount = () => {};
541 -
542 - render() {
543 - return <div />;
544 - }
545 - }
546 -
547 - expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toErrorDev(
548 - 'Warning: Component has a method called ' +
549 - 'componentDidUnmount(). But there is no such lifecycle method. ' +
550 - 'Did you mean componentWillUnmount()?',
551 - );
552 - });
553 -
554 - it('should warn when componentDidReceiveProps method is defined', () => {
555 - class Component extends React.Component {
556 - componentDidReceiveProps = () => {};
557 -
558 - render() {
559 - return <div />;
560 - }
561 - }
562 -
563 - expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toErrorDev(
564 - 'Warning: Component has a method called ' +
565 - 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
566 - 'If you meant to update the state in response to changing props, ' +
567 - 'use componentWillReceiveProps(). If you meant to fetch data or ' +
568 - 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
569 - );
570 - });
571 -
572 - it('should warn when defaultProps was defined as an instance property', () => {
573 - class Component extends React.Component {
574 - constructor(props) {
575 - super(props);
576 - this.defaultProps = {name: 'Abhay'};
577 - }
578 -
579 - render() {
580 - return <div />;
581 - }
582 - }
583 -
584 - expect(() => ReactTestUtils.renderIntoDocument(<Component />)).toErrorDev(
585 - 'Warning: Setting defaultProps as an instance property on Component is not supported ' +
586 - 'and will be ignored. Instead, define defaultProps as a static property on Component.',
587 - );
588 - });
589 -
590 - // @gate !disableLegacyContext
591 - it('should pass context to children when not owner', () => {
592 - class Parent extends React.Component {
593 - render() {
594 - return (
595 - <Child>
596 - <Grandchild />
597 - </Child>
598 - );
599 - }
600 - }
601 -
602 - class Child extends React.Component {
603 - static childContextTypes = {
604 - foo: PropTypes.string,
605 - };
606 -
607 - getChildContext() {
608 - return {
609 - foo: 'bar',
610 - };
611 - }
612 -
613 - render() {
614 - return React.Children.only(this.props.children);
615 - }
616 - }
617 -
618 - class Grandchild extends React.Component {
619 - static contextTypes = {
620 - foo: PropTypes.string,
621 - };
622 -
623 - render() {
624 - return <div>{this.context.foo}</div>;
625 - }
626 - }
627 -
628 - const component = ReactTestUtils.renderIntoDocument(<Parent />);
629 - expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar');
630 - });
631 -
632 - it('should skip update when rerendering element in container', () => {
633 - class Parent extends React.Component {
634 - render() {
635 - return <div>{this.props.children}</div>;
636 - }
637 - }
638 -
639 - let childRenders = 0;
640 -
641 - class Child extends React.Component {
642 - render() {
643 - childRenders++;
644 - return <div />;
645 - }
646 - }
647 -
648 - const container = document.createElement('div');
649 - const child = <Child />;
650 -
651 - ReactDOM.render(<Parent>{child}</Parent>, container);
652 - ReactDOM.render(<Parent>{child}</Parent>, container);
653 - expect(childRenders).toBe(1);
654 - });
655 -
656 - // @gate !disableLegacyContext
657 - it('should pass context when re-rendered for static child', () => {
658 - let parentInstance = null;
659 - let childInstance = null;
660 -
661 - class Parent extends React.Component {
662 - static childContextTypes = {
663 - foo: PropTypes.string,
664 - flag: PropTypes.bool,
665 - };
666 -
667 - state = {
668 - flag: false,
669 - };
670 -
671 - getChildContext() {
672 - return {
673 - foo: 'bar',
674 - flag: this.state.flag,
675 - };
676 - }
677 -
678 - render() {
679 - return React.Children.only(this.props.children);
680 - }
681 - }
682 -
683 - class Middle extends React.Component {
684 - render() {
685 - return this.props.children;
686 - }
687 - }
688 -
689 - class Child extends React.Component {
690 - static contextTypes = {
691 - foo: PropTypes.string,
692 - flag: PropTypes.bool,
693 - };
694 -
695 - render() {
696 - childInstance = this;
697 - return <span>Child</span>;
698 - }
699 - }
700 -
701 - parentInstance = ReactTestUtils.renderIntoDocument(
702 - <Parent>
703 - <Middle>
704 - <Child />
705 - </Middle>
706 - </Parent>,
707 - );
708 -
709 - expect(parentInstance.state.flag).toBe(false);
710 - expect(childInstance.context).toEqual({foo: 'bar', flag: false});
711 -
712 - parentInstance.setState({flag: true});
713 - expect(parentInstance.state.flag).toBe(true);
714 - expect(childInstance.context).toEqual({foo: 'bar', flag: true});
715 - });
716 -
717 - // @gate !disableLegacyContext
718 - it('should pass context when re-rendered for static child within a composite component', () => {
719 - class Parent extends React.Component {
720 - static childContextTypes = {
721 - flag: PropTypes.bool,
722 - };
723 -
724 - state = {
725 - flag: true,
726 - };
727 -
728 - getChildContext() {
729 - return {
730 - flag: this.state.flag,
731 - };
732 - }
733 -
734 - render() {
735 - return <div>{this.props.children}</div>;
736 - }
737 - }
738 -
739 - class Child extends React.Component {
740 - static contextTypes = {
741 - flag: PropTypes.bool,
742 - };
743 -
744 - render() {
745 - return <div />;
746 - }
747 - }
748 -
749 - class Wrapper extends React.Component {
750 - parentRef = React.createRef();
751 - childRef = React.createRef();
752 -
753 - render() {
754 - return (
755 - <Parent ref={this.parentRef}>
756 - <Child ref={this.childRef} />
757 - </Parent>
758 - );
759 - }
760 - }
761 -
762 - const wrapper = ReactTestUtils.renderIntoDocument(<Wrapper />);
763 -
764 - expect(wrapper.parentRef.current.state.flag).toEqual(true);
765 - expect(wrapper.childRef.current.context).toEqual({flag: true});
766 -
767 - // We update <Parent /> while <Child /> is still a static prop relative to this update
768 - wrapper.parentRef.current.setState({flag: false});
769 -
770 - expect(wrapper.parentRef.current.state.flag).toEqual(false);
771 - expect(wrapper.childRef.current.context).toEqual({flag: false});
772 - });
773 -
774 - // @gate !disableLegacyContext
775 - it('should pass context transitively', () => {
776 - let childInstance = null;
777 - let grandchildInstance = null;
778 -
779 - class Parent extends React.Component {
780 - static childContextTypes = {
781 - foo: PropTypes.string,
782 - depth: PropTypes.number,
783 - };
784 -
785 - getChildContext() {
786 - return {
787 - foo: 'bar',
788 - depth: 0,
789 - };
790 - }
791 -
792 - render() {
793 - return <Child />;
794 - }
795 - }
796 -
797 - class Child extends React.Component {
798 - static contextTypes = {
799 - foo: PropTypes.string,
800 - depth: PropTypes.number,
801 - };
802 -
803 - static childContextTypes = {
804 - depth: PropTypes.number,
805 - };
806 -
807 - getChildContext() {
808 - return {
809 - depth: this.context.depth + 1,
810 - };
811 - }
812 -
813 - render() {
814 - childInstance = this;
815 - return <Grandchild />;
816 - }
817 - }
818 -
819 - class Grandchild extends React.Component {
820 - static contextTypes = {
821 - foo: PropTypes.string,
822 - depth: PropTypes.number,
823 - };
824 -
825 - render() {
826 - grandchildInstance = this;
827 - return <div />;
828 - }
829 - }
830 -
831 - ReactTestUtils.renderIntoDocument(<Parent />);
832 - expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
833 - expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1});
834 - });
835 -
836 - // @gate !disableLegacyContext
837 - it('should pass context when re-rendered', () => {
838 - let parentInstance = null;
839 - let childInstance = null;
840 -
841 - class Parent extends React.Component {
842 - static childContextTypes = {
843 - foo: PropTypes.string,
844 - depth: PropTypes.number,
845 - };
846 -
847 - state = {
848 - flag: false,
849 - };
850 -
851 - getChildContext() {
852 - return {
853 - foo: 'bar',
854 - depth: 0,
855 - };
856 - }
857 -
858 - render() {
859 - let output = <Child />;
860 - if (!this.state.flag) {
861 - output = <span>Child</span>;
862 - }
863 - return output;
864 - }
865 - }
866 -
867 - class Child extends React.Component {
868 - static contextTypes = {
869 - foo: PropTypes.string,
870 - depth: PropTypes.number,
871 - };
872 -
873 - render() {
874 - childInstance = this;
875 - return <span>Child</span>;
876 - }
877 - }
878 -
879 - parentInstance = ReactTestUtils.renderIntoDocument(<Parent />);
880 - expect(childInstance).toBeNull();
881 -
882 - expect(parentInstance.state.flag).toBe(false);
883 - ReactDOM.unstable_batchedUpdates(function () {
884 - parentInstance.setState({flag: true});
885 - });
886 - expect(parentInstance.state.flag).toBe(true);
887 -
888 - expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
889 - });
890 -
891 - // @gate !disableLegacyContext
892 - it('unmasked context propagates through updates', () => {
893 - class Leaf extends React.Component {
894 - static contextTypes = {
895 - foo: PropTypes.string.isRequired,
896 - };
897 -
898 - UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
899 - expect('foo' in nextContext).toBe(true);
900 - }
901 -
902 - shouldComponentUpdate(nextProps, nextState, nextContext) {
903 - expect('foo' in nextContext).toBe(true);
904 - return true;
905 - }
906 -
907 - render() {
908 - return <span>{this.context.foo}</span>;
909 - }
910 - }
911 -
912 - class Intermediary extends React.Component {
913 - UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
914 - expect('foo' in nextContext).toBe(false);
915 - }
916 -
917 - shouldComponentUpdate(nextProps, nextState, nextContext) {
918 - expect('foo' in nextContext).toBe(false);
919 - return true;
920 - }
921 -
922 - render() {
923 - return <Leaf />;
924 - }
925 - }
926 -
927 - class Parent extends React.Component {
928 - static childContextTypes = {
929 - foo: PropTypes.string,
930 - };
931 -
932 - getChildContext() {
933 - return {
934 - foo: this.props.cntxt,
935 - };
602 + componentWillUnmount() {
603 + innerUnmounted = true;
604 }
605
606 render() {
939 - return <Intermediary />;
607 + return <div />;
608 }
609 }
610
943 - const div = document.createElement('div');
944 - ReactDOM.render(<Parent cntxt="noise" />, div);
945 - expect(div.children[0].innerHTML).toBe('noise');
946 - div.children[0].innerHTML = 'aliens';
947 - div.children[0].id = 'aliens';
948 - expect(div.children[0].innerHTML).toBe('aliens');
949 - expect(div.children[0].id).toBe('aliens');
950 - ReactDOM.render(<Parent cntxt="bar" />, div);
951 - expect(div.children[0].innerHTML).toBe('bar');
952 - expect(div.children[0].id).toBe('aliens');
611 + const root = ReactDOMClient.createRoot(container);
612 + await act(() => {
613 + root.render(<Component />);
614 + });
615 + root.unmount();
616 + expect(innerUnmounted).toBe(true);
617 });
618
955 - // @gate !disableLegacyContext
956 - it('should trigger componentWillReceiveProps for context changes', () => {
957 - let contextChanges = 0;
958 - let propChanges = 0;
959 -
960 - class GrandChild extends React.Component {
961 - static contextTypes = {
962 - foo: PropTypes.string.isRequired,
963 - };
964 -
965 - UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
966 - expect('foo' in nextContext).toBe(true);
967 -
968 - if (nextProps !== this.props) {
969 - propChanges++;
970 - }
619 + it('should warn when shouldComponentUpdate() returns undefined', async () => {
620 + class ClassComponent extends React.Component {
621 + state = {bogus: false};
622
972 - if (nextContext !== this.context) {
973 - contextChanges++;
974 - }
623 + shouldComponentUpdate() {
624 + return undefined;
625 }
626
627 render() {
978 - return <span className="grand-child">{this.props.children}</span>;
628 + return <div />;
629 }
630 }
631 + let instance;
632 + const root = ReactDOMClient.createRoot(document.createElement('div'));
633 + await act(() => {
634 + root.render(<ClassComponent ref={ref => (instance = ref)} />);
635 + });
636
982 - class ChildWithContext extends React.Component {
983 - static contextTypes = {
984 - foo: PropTypes.string.isRequired,
985 - };
986 -
987 - UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
988 - expect('foo' in nextContext).toBe(true);
637 + expect(() => {
638 + ReactDOM.flushSync(() => {
639 + instance.setState({bogus: true});
640 + });
641 + }).toErrorDev(
642 + 'Warning: ClassComponent.shouldComponentUpdate(): Returned undefined instead of a ' +
643 + 'boolean value. Make sure to return true or false.',
644 + );
645 + });
646
990 - if (nextProps !== this.props) {
991 - propChanges++;
992 - }
647 + it('should warn when componentDidUnmount method is defined', async () => {
648 + class Component extends React.Component {
649 + componentDidUnmount = () => {};
650
994 - if (nextContext !== this.context) {
995 - contextChanges++;
996 - }
651 + render() {
652 + return <div />;
653 }
654 + }
655 +
656 + const root = ReactDOMClient.createRoot(document.createElement('div'));
657 + expect(() => {
658 + ReactDOM.flushSync(() => {
659 + root.render(<Component />);
660 + });
661 + }).toErrorDev(
662 + 'Warning: Component has a method called ' +
663 + 'componentDidUnmount(). But there is no such lifecycle method. ' +
664 + 'Did you mean componentWillUnmount()?',
665 + );
666 + });
667 +
668 + it('should warn when componentDidReceiveProps method is defined', () => {
669 + class Component extends React.Component {
670 + componentDidReceiveProps = () => {};
671
672 render() {
1000 - return <div className="child-with">{this.props.children}</div>;
673 + return <div />;
674 }
675 }
676
1004 - class ChildWithoutContext extends React.Component {
1005 - UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
1006 - expect('foo' in nextContext).toBe(false);
677 + const root = ReactDOMClient.createRoot(document.createElement('div'));
678
1008 - if (nextProps !== this.props) {
1009 - propChanges++;
1010 - }
679 + expect(() => {
680 + ReactDOM.flushSync(() => {
681 + root.render(<Component />);
682 + });
683 + }).toErrorDev(
684 + 'Warning: Component has a method called ' +
685 + 'componentDidReceiveProps(). But there is no such lifecycle method. ' +
686 + 'If you meant to update the state in response to changing props, ' +
687 + 'use componentWillReceiveProps(). If you meant to fetch data or ' +
688 + 'run side-effects or mutations after React has updated the UI, use componentDidUpdate().',
689 + );
690 + });
691
1012 - if (nextContext !== this.context) {
1013 - contextChanges++;
1014 - }
692 + it('should warn when defaultProps was defined as an instance property', () => {
693 + class Component extends React.Component {
694 + constructor(props) {
695 + super(props);
696 + this.defaultProps = {name: 'Abhay'};
697 }
698
699 render() {
1018 - return <div className="child-without">{this.props.children}</div>;
700 + return <div />;
701 }
702 }
703 + const root = ReactDOMClient.createRoot(document.createElement('div'));
704
1022 - class Parent extends React.Component {
1023 - static childContextTypes = {
1024 - foo: PropTypes.string,
1025 - };
1026 -
1027 - state = {
1028 - foo: 'abc',
1029 - };
705 + expect(() => {
706 + ReactDOM.flushSync(() => {
707 + root.render(<Component />);
708 + });
709 + }).toErrorDev(
710 + 'Warning: Setting defaultProps as an instance property on Component is not supported ' +
711 + 'and will be ignored. Instead, define defaultProps as a static property on Component.',
712 + );
713 + });
714
1031 - getChildContext() {
1032 - return {
1033 - foo: this.state.foo,
1034 - };
715 + it('should skip update when rerendering element in container', async () => {
716 + class Parent extends React.Component {
717 + render() {
718 + return <div>{this.props.children}</div>;
719 }
720 + }
721
722 + class Child extends React.Component {
723 render() {
1038 - return <div className="parent">{this.props.children}</div>;
724 + Scheduler.log('Child render');
725 + return <div />;
726 }
727 }
728
1042 - const div = document.createElement('div');
1043 -
1044 - let parentInstance = null;
1045 - ReactDOM.render(
1046 - <Parent ref={inst => (parentInstance = inst)}>
1047 - <ChildWithoutContext>
1048 - A1
1049 - <GrandChild>A2</GrandChild>
1050 - </ChildWithoutContext>
1051 -
1052 - <ChildWithContext>
1053 - B1
1054 - <GrandChild>B2</GrandChild>
1055 - </ChildWithContext>
1056 - </Parent>,
1057 - div,
1058 - );
1059 -
1060 - parentInstance.setState({
1061 - foo: 'def',
729 + const container = document.createElement('div');
730 + const child = <Child />;
731 + const root = ReactDOMClient.createRoot(container);
732 + await act(() => {
733 + root.render(<Parent>{child}</Parent>);
734 });
735 + assertLog(['Child render']);
736
1064 - expect(propChanges).toBe(0);
1065 - expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
737 + await act(() => {
738 + root.render(<Parent>{child}</Parent>);
739 + });
740 + assertLog([]);
741 });
742
743 it('should disallow nested render calls', () => {
744 + const root = ReactDOMClient.createRoot(document.createElement('div'));
745 class Inner extends React.Component {
746 render() {
747 return <div />;
@@ -1074,12 +750,16 @@ describe('ReactCompositeComponent', () => {
750
751 class Outer extends React.Component {
752 render() {
1077 - ReactTestUtils.renderIntoDocument(<Inner />);
753 + root.render(<Inner />);
754 return <div />;
755 }
756 }
757
1082 - expect(() => ReactTestUtils.renderIntoDocument(<Outer />)).toErrorDev(
758 + expect(() => {
759 + ReactDOM.flushSync(() => {
760 + root.render(<Outer />);
761 + });
762 + }).toErrorDev(
763 'Render methods should be a pure function of props and state; ' +
764 'triggering nested component updates from render is not allowed. If ' +
765 'necessary, trigger nested updates in componentDidUpdate.\n\nCheck the ' +
@@ -1087,7 +767,7 @@ describe('ReactCompositeComponent', () => {
767 );
768 });
769
1090 - it('only renders once if updated in componentWillReceiveProps', () => {
770 + it('only renders once if updated in componentWillReceiveProps', async () => {
771 let renders = 0;
772
773 class Component extends React.Component {
@@ -1107,15 +787,23 @@ describe('ReactCompositeComponent', () => {
787 }
788
789 const container = document.createElement('div');
1110 - const instance = ReactDOM.render(<Component update={0} />, container);
790 + const root = ReactDOMClient.createRoot(container);
791 + let instance;
792 +
793 + await act(() => {
794 + root.render(<Component update={0} ref={ref => (instance = ref)} />);
795 + });
796 expect(renders).toBe(1);
797 expect(instance.state.updated).toBe(false);
1113 - ReactDOM.render(<Component update={1} />, container);
798 +
799 + await act(() => {
800 + root.render(<Component update={1} ref={ref => (instance = ref)} />);
801 + });
802 expect(renders).toBe(2);
803 expect(instance.state.updated).toBe(true);
804 });
805
1118 - it('only renders once if updated in componentWillReceiveProps when batching', () => {
806 + it('only renders once if updated in componentWillReceiveProps when batching', async () => {
807 let renders = 0;
808
809 class Component extends React.Component {
@@ -1135,234 +823,21 @@ describe('ReactCompositeComponent', () => {
823 }
824
825 const container = document.createElement('div');
1138 - const instance = ReactDOM.render(<Component update={0} />, container);
826 + const root = ReactDOMClient.createRoot(container);
827 + let instance;
828 + await act(() => {
829 + root.render(<Component update={0} ref={ref => (instance = ref)} />);
830 + });
831 expect(renders).toBe(1);
832 expect(instance.state.updated).toBe(false);
1141 - ReactDOM.unstable_batchedUpdates(() => {
1142 - ReactDOM.render(<Component update={1} />, container);
833 + await act(() => {
834 + root.render(<Component update={1} ref={ref => (instance = ref)} />);
835 });
836 expect(renders).toBe(2);
837 expect(instance.state.updated).toBe(true);
838 });
839
1148 - it('should update refs if shouldComponentUpdate gives false', () => {
1149 - class Static extends React.Component {
1150 - shouldComponentUpdate() {
1151 - return false;
1152 - }
1153 -
1154 - render() {
1155 - return <div>{this.props.children}</div>;
1156 - }
1157 - }
1158 -
1159 - class Component extends React.Component {
1160 - static0Ref = React.createRef();
1161 - static1Ref = React.createRef();
1162 -
1163 - render() {
1164 - if (this.props.flipped) {
1165 - return (
1166 - <div>
1167 - <Static ref={this.static0Ref} key="B">
1168 - B (ignored)
1169 - </Static>
1170 - <Static ref={this.static1Ref} key="A">
1171 - A (ignored)
1172 - </Static>
1173 - </div>
1174 - );
1175 - } else {
1176 - return (
1177 - <div>
1178 - <Static ref={this.static0Ref} key="A">
1179 - A
1180 - </Static>
1181 - <Static ref={this.static1Ref} key="B">
1182 - B
1183 - </Static>
1184 - </div>
1185 - );
1186 - }
1187 - }
1188 - }
1189 -
1190 - const container = document.createElement('div');
1191 - const comp = ReactDOM.render(<Component flipped={false} />, container);
1192 - expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A');
1193 - expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B');
1194 -
1195 - // When flipping the order, the refs should update even though the actual
1196 - // contents do not
1197 - ReactDOM.render(<Component flipped={true} />, container);
1198 - expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B');
1199 - expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A');
1200 - });
1201 -
1202 - it('should allow access to findDOMNode in componentWillUnmount', () => {
1203 - let a = null;
1204 - let b = null;
1205 -
1206 - class Component extends React.Component {
1207 - componentDidMount() {
1208 - a = ReactDOM.findDOMNode(this);
1209 - expect(a).not.toBe(null);
1210 - }
1211 -
1212 - componentWillUnmount() {
1213 - b = ReactDOM.findDOMNode(this);
1214 - expect(b).not.toBe(null);
1215 - }
1216 -
1217 - render() {
1218 - return <div />;
1219 - }
1220 - }
1221 -
1222 - const container = document.createElement('div');
1223 - expect(a).toBe(container.firstChild);
1224 - ReactDOM.render(<Component />, container);
1225 - ReactDOM.unmountComponentAtNode(container);
1226 - expect(a).toBe(b);
1227 - });
1228 -
1229 - // @gate !disableLegacyContext || !__DEV__
1230 - it('context should be passed down from the parent', () => {
1231 - class Parent extends React.Component {
1232 - static childContextTypes = {
1233 - foo: PropTypes.string,
1234 - };
1235 -
1236 - getChildContext() {
1237 - return {
1238 - foo: 'bar',
1239 - };
1240 - }
1241 -
1242 - render() {
1243 - return <div>{this.props.children}</div>;
1244 - }
1245 - }
1246 -
1247 - class Component extends React.Component {
1248 - static contextTypes = {
1249 - foo: PropTypes.string.isRequired,
1250 - };
1251 -
1252 - render() {
1253 - return <div />;
1254 - }
1255 - }
1256 -
1257 - const div = document.createElement('div');
1258 - ReactDOM.render(
1259 - <Parent>
1260 - <Component />
1261 - </Parent>,
1262 - div,
1263 - );
1264 - });
1265 -
1266 - it('should replace state', () => {
1267 - class Moo extends React.Component {
1268 - state = {x: 1};
1269 - render() {
1270 - return <div />;
1271 - }
1272 - }
1273 -
1274 - const moo = ReactTestUtils.renderIntoDocument(<Moo />);
1275 - // No longer a public API, but we can test that it works internally by
1276 - // reaching into the updater.
1277 - moo.updater.enqueueReplaceState(moo, {y: 2});
1278 - expect('x' in moo.state).toBe(false);
1279 - expect(moo.state.y).toBe(2);
1280 - });
1281 -
1282 - it('should support objects with prototypes as state', () => {
1283 - const NotActuallyImmutable = function (str) {
1284 - this.str = str;
1285 - };
1286 - NotActuallyImmutable.prototype.amIImmutable = function () {
1287 - return true;
1288 - };
1289 - class Moo extends React.Component {
1290 - state = new NotActuallyImmutable('first');
1291 - // No longer a public API, but we can test that it works internally by
1292 - // reaching into the updater.
1293 - _replaceState = update => this.updater.enqueueReplaceState(this, update);
1294 - render() {
1295 - return <div />;
1296 - }
1297 - }
1298 -
1299 - const moo = ReactTestUtils.renderIntoDocument(<Moo />);
1300 - expect(moo.state.str).toBe('first');
1301 - expect(moo.state.amIImmutable()).toBe(true);
1302 -
1303 - const secondState = new NotActuallyImmutable('second');
1304 - moo._replaceState(secondState);
1305 - expect(moo.state.str).toBe('second');
1306 - expect(moo.state.amIImmutable()).toBe(true);
1307 - expect(moo.state).toBe(secondState);
1308 -
1309 - moo.setState({str: 'third'});
1310 - expect(moo.state.str).toBe('third');
1311 - // Here we lose the prototype.
1312 - expect(moo.state.amIImmutable).toBe(undefined);
1313 -
1314 - // When more than one state update is enqueued, we have the same behavior
1315 - const fifthState = new NotActuallyImmutable('fifth');
1316 - ReactDOM.unstable_batchedUpdates(function () {
1317 - moo.setState({str: 'fourth'});
1318 - moo._replaceState(fifthState);
1319 - });
1320 - expect(moo.state).toBe(fifthState);
1321 -
1322 - // When more than one state update is enqueued, we have the same behavior
1323 - const sixthState = new NotActuallyImmutable('sixth');
1324 - ReactDOM.unstable_batchedUpdates(function () {
1325 - moo._replaceState(sixthState);
1326 - moo.setState({str: 'seventh'});
1327 - });
1328 - expect(moo.state.str).toBe('seventh');
1329 - expect(moo.state.amIImmutable).toBe(undefined);
1330 - });
1331 -
1332 - it('should not warn about unmounting during unmounting', () => {
1333 - const container = document.createElement('div');
1334 - const layer = document.createElement('div');
1335 -
1336 - class Component extends React.Component {
1337 - componentDidMount() {
1338 - ReactDOM.render(<div />, layer);
1339 - }
1340 -
1341 - componentWillUnmount() {
1342 - ReactDOM.unmountComponentAtNode(layer);
1343 - }
1344 -
1345 - render() {
1346 - return <div />;
1347 - }
1348 - }
1349 -
1350 - class Outer extends React.Component {
1351 - render() {
1352 - return <div>{this.props.children}</div>;
1353 - }
1354 - }
1355 -
1356 - ReactDOM.render(
1357 - <Outer>
1358 - <Component />
1359 - </Outer>,
1360 - container,
1361 - );
1362 - ReactDOM.render(<Outer />, container);
1363 - });
1364 -
1365 - it('should warn when mutated props are passed', () => {
840 + it('should warn when mutated props are passed', async () => {
841 const container = document.createElement('div');
842
843 class Foo extends React.Component {
@@ -1376,7 +851,12 @@ describe('ReactCompositeComponent', () => {
851 }
852 }
853
1379 - expect(() => ReactDOM.render(<Foo idx="qwe" />, container)).toErrorDev(
854 + const root = ReactDOMClient.createRoot(container);
855 + expect(() => {
856 + ReactDOM.flushSync(() => {
857 + root.render(<Foo idx="qwe" />);
858 + });
859 + }).toErrorDev(
860 'Foo(...): When calling super() in `Foo`, make sure to pass ' +
861 "up the same props that your component's constructor was passed.",
862 );
@@ -1416,29 +896,32 @@ describe('ReactCompositeComponent', () => {
896 }
897 };
898
899 + const root = ReactDOMClient.createRoot(container);
900 expect(() => {
1420 - ReactDOM.render(<App ref={setRef} stage={1} />, container);
1421 - ReactDOM.render(<App ref={setRef} stage={2} />, container);
901 + ReactDOM.flushSync(() => {
902 + root.render(<App ref={setRef} stage={1} />);
903 + });
904 + ReactDOM.flushSync(() => {
905 + root.render(<App ref={setRef} stage={2} />);
906 + });
907 }).toThrow();
908 expect(count).toBe(1);
909 });
910
1426 - it('prepares new child before unmounting old', () => {
1427 - const log = [];
1428 -
911 + it('prepares new child before unmounting old', async () => {
912 class Spy extends React.Component {
913 UNSAFE_componentWillMount() {
1431 - log.push(this.props.name + ' componentWillMount');
914 + Scheduler.log(this.props.name + ' componentWillMount');
915 }
916 render() {
1434 - log.push(this.props.name + ' render');
917 + Scheduler.log(this.props.name + ' render');
918 return <div />;
919 }
920 componentDidMount() {
1438 - log.push(this.props.name + ' componentDidMount');
921 + Scheduler.log(this.props.name + ' componentDidMount');
922 }
923 componentWillUnmount() {
1441 - log.push(this.props.name + ' componentWillUnmount');
924 + Scheduler.log(this.props.name + ' componentWillUnmount');
925 }
926 }
927
@@ -1449,10 +932,15 @@ describe('ReactCompositeComponent', () => {
932 }
933
934 const container = document.createElement('div');
1452 - ReactDOM.render(<Wrapper name="A" />, container);
1453 - ReactDOM.render(<Wrapper name="B" />, container);
935 + const root = ReactDOMClient.createRoot(container);
936 + await act(() => {
937 + root.render(<Wrapper name="A" />);
938 + });
939 + await act(() => {
940 + root.render(<Wrapper name="B" />);
941 + });
942
1455 - expect(log).toEqual([
943 + assertLog([
944 'A componentWillMount',
945 'A render',
946 'A componentDidMount',
@@ -1464,8 +952,7 @@ describe('ReactCompositeComponent', () => {
952 ]);
953 });
954
1467 - it('respects a shallow shouldComponentUpdate implementation', () => {
1468 - let renderCalls = 0;
955 + it('respects a shallow shouldComponentUpdate implementation', async () => {
956 class PlasticWrap extends React.Component {
957 constructor(props, context) {
958 super(props, context);
@@ -1504,37 +991,54 @@ describe('ReactCompositeComponent', () => {
991 }
992
993 render() {
1507 - renderCalls++;
994 + const {color} = this.props;
995 + const {cut, slices} = this.state;
996 +
997 + Scheduler.log(`${color} ${cut} ${slices}`);
998 return <div />;
999 }
1000 }
1001
1002 const container = document.createElement('div');
1513 - const instance = ReactDOM.render(<PlasticWrap />, container);
1514 - expect(renderCalls).toBe(1);
1003 + const root = ReactDOMClient.createRoot(container);
1004 + let instance;
1005 + await act(() => {
1006 + root.render(<PlasticWrap ref={ref => (instance = ref)} />);
1007 + });
1008 + assertLog(['green false 1']);
1009
1010 // Do not re-render based on props
1517 - instance.setState({color: 'green'});
1518 - expect(renderCalls).toBe(1);
1011 + await act(() => {
1012 + instance.setState({color: 'green'});
1013 + });
1014 + assertLog([]);
1015
1016 // Re-render based on props
1521 - instance.setState({color: 'red'});
1522 - expect(renderCalls).toBe(2);
1017 + await act(() => {
1018 + instance.setState({color: 'red'});
1019 + });
1020 + assertLog(['red false 1']);
1021
1022 // Re-render base on state
1525 - instance.appleRef.current.cut();
1526 - expect(renderCalls).toBe(3);
1023 + await act(() => {
1024 + instance.appleRef.current.cut();
1025 + });
1026 + assertLog(['red true 10']);
1027
1028 // No re-render based on state
1529 - instance.appleRef.current.cut();
1530 - expect(renderCalls).toBe(3);
1029 + await act(() => {
1030 + instance.appleRef.current.cut();
1031 + });
1032 + assertLog([]);
1033
1034 // Re-render based on state again
1533 - instance.appleRef.current.eatSlice();
1534 - expect(renderCalls).toBe(4);
1035 + await act(() => {
1036 + instance.appleRef.current.eatSlice();
1037 + });
1038 + assertLog(['red true 9']);
1039 });
1040
1537 - it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', () => {
1041 + it('does not do a deep comparison for a shallow shouldComponentUpdate implementation', async () => {
1042 function getInitialState() {
1043 return {
1044 foo: [1, 2, 3],
@@ -1542,7 +1046,6 @@ describe('ReactCompositeComponent', () => {
1046 };
1047 }
1048
1545 - let renderCalls = 0;
1049 const initialSettings = getInitialState();
1050
1051 class Component extends React.Component {
@@ -1553,34 +1056,45 @@ describe('ReactCompositeComponent', () => {
1056 }
1057
1058 render() {
1556 - renderCalls++;
1059 + const {foo, bar} = this.state;
1060 + Scheduler.log(`{foo:[${foo}],bar:{a:${bar.a},b:${bar.b},c:${bar.c}}`);
1061 return <div />;
1062 }
1063 }
1064
1065 const container = document.createElement('div');
1562 - const instance = ReactDOM.render(<Component />, container);
1563 - expect(renderCalls).toBe(1);
1066 + const root = ReactDOMClient.createRoot(container);
1067 + let instance;
1068 + await act(() => {
1069 + root.render(<Component ref={ref => (instance = ref)} />);
1070 + });
1071 + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1072
1073 // Do not re-render if state is equal
1074 const settings = {
1075 foo: initialSettings.foo,
1076 bar: initialSettings.bar,
1077 };
1570 - instance.setState(settings);
1571 - expect(renderCalls).toBe(1);
1078 + await act(() => {
1079 + instance.setState(settings);
1080 + });
1081 + assertLog([]);
1082
1083 // Re-render because one field changed
1084 initialSettings.foo = [1, 2, 3];
1575 - instance.setState(initialSettings);
1576 - expect(renderCalls).toBe(2);
1085 + await act(() => {
1086 + instance.setState(initialSettings);
1087 + });
1088 + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1089
1090 // Re-render because the object changed
1579 - instance.setState(getInitialState());
1580 - expect(renderCalls).toBe(3);
1091 + await act(() => {
1092 + instance.setState(getInitialState());
1093 + });
1094 + assertLog(['{foo:[1,2,3],bar:{a:4,b:5,c:6}']);
1095 });
1096
1583 - it('should call setState callback with no arguments', () => {
1097 + it('should call setState callback with no arguments', async () => {
1098 let mockArgs;
1099 class Component extends React.Component {
1100 componentDidMount() {
@@ -1590,12 +1104,15 @@ describe('ReactCompositeComponent', () => {
1104 return false;
1105 }
1106 }
1107 + const root = ReactDOMClient.createRoot(document.createElement('div'));
1108 + await act(() => {
1109 + root.render(<Component />);
1110 + });
1111
1594 - ReactTestUtils.renderIntoDocument(<Component />);
1112 expect(mockArgs.length).toEqual(0);
1113 });
1114
1598 - it('this.state should be updated on setState callback inside componentWillMount', () => {
1115 + it('this.state should be updated on setState callback inside componentWillMount', async () => {
1116 const div = document.createElement('div');
1117 let stateSuccessfullyUpdated = false;
1118
@@ -1619,16 +1136,18 @@ describe('ReactCompositeComponent', () => {
1136 }
1137 }
1138
1622 - ReactDOM.render(<Component />, div);
1139 + const root = ReactDOMClient.createRoot(div);
1140 + await act(() => {
1141 + root.render(<Component />);
1142 + });
1143 +
1144 expect(stateSuccessfullyUpdated).toBe(true);
1145 });
1146
1626 - it('should call the setState callback even if shouldComponentUpdate = false', done => {
1147 + it('should call the setState callback even if shouldComponentUpdate = false', async () => {
1148 const mockFn = jest.fn().mockReturnValue(false);
1149 const div = document.createElement('div');
1150
1630 - let instance;
1631 -
1151 class Component extends React.Component {
1152 constructor(props, context) {
1153 super(props, context);
@@ -1650,16 +1169,24 @@ describe('ReactCompositeComponent', () => {
1169 }
1170 }
1171
1653 - ReactDOM.render(<Component />, div);
1172 + const root = ReactDOMClient.createRoot(div);
1173 + let instance;
1174 + await act(() => {
1175 + root.render(<Component ref={ref => (instance = ref)} />);
1176 + });
1177
1178 expect(instance).toBeDefined();
1179 expect(mockFn).not.toBeCalled();
1180
1658 - instance.setState({hasUpdatedState: true}, () => {
1659 - expect(mockFn).toBeCalled();
1660 - expect(instance.state.hasUpdatedState).toBe(true);
1661 - done();
1181 + await act(() => {
1182 + instance.setState({hasUpdatedState: true}, () => {
1183 + expect(mockFn).toBeCalled();
1184 + expect(instance.state.hasUpdatedState).toBe(true);
1185 + Scheduler.log('setState callback called');
1186 + });
1187 });
1188 +
1189 + assertLog(['setState callback called']);
1190 });
1191
1192 it('should return a meaningful warning when constructor is returned', () => {
@@ -1674,9 +1201,12 @@ describe('ReactCompositeComponent', () => {
1201 }
1202 }
1203
1204 + const root = ReactDOMClient.createRoot(document.createElement('div'));
1205 expect(() => {
1206 expect(() => {
1679 - ReactTestUtils.renderIntoDocument(<RenderTextInvalidConstructor />);
1207 + ReactDOM.flushSync(() => {
1208 + root.render(<RenderTextInvalidConstructor />);
1209 + });
1210 }).toThrow();
1211 }).toErrorDev([
1212 // Expect two errors because invokeGuardedCallback will dispatch an error event,
@@ -1685,6 +1215,11 @@ describe('ReactCompositeComponent', () => {
1215 'did you accidentally return an object from the constructor?',
1216 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
1217 'did you accidentally return an object from the constructor?',
1218 + // And then two more because we retry errors.
1219 + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
1220 + 'did you accidentally return an object from the constructor?',
1221 + 'Warning: RenderTextInvalidConstructor(...): No `render` method found on the returned component instance: ' +
1222 + 'did you accidentally return an object from the constructor?',
1223 ]);
1224 });
1225
@@ -1699,8 +1234,11 @@ describe('ReactCompositeComponent', () => {
1234 }
1235
1236 const container = document.createElement('div');
1237 + const root = ReactDOMClient.createRoot(container);
1238 expect(() => {
1703 - ReactDOM.render(<Bad />, container);
1239 + ReactDOM.flushSync(() => {
1240 + root.render(<Bad />);
1241 + });
1242 }).toErrorDev(
1243 'It looks like Bad is reassigning its own `this.props` while rendering. ' +
1244 'This is not supported and can lead to confusing bugs.',
@@ -1710,9 +1248,12 @@ describe('ReactCompositeComponent', () => {
1248 it('should return error if render is not defined', () => {
1249 class RenderTestUndefinedRender extends React.Component {}
1250
1251 + const root = ReactDOMClient.createRoot(document.createElement('div'));
1252 expect(() => {
1253 expect(() => {
1715 - ReactTestUtils.renderIntoDocument(<RenderTestUndefinedRender />);
1254 + ReactDOM.flushSync(() => {
1255 + root.render(<RenderTestUndefinedRender />);
1256 + });
1257 }).toThrow();
1258 }).toErrorDev([
1259 // Expect two errors because invokeGuardedCallback will dispatch an error event,
@@ -1721,12 +1262,18 @@ describe('ReactCompositeComponent', () => {
1262 'component instance: you may have forgotten to define `render`.',
1263 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
1264 'component instance: you may have forgotten to define `render`.',
1265 +
1266 + // And then two more because we retry errors.
1267 + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
1268 + 'component instance: you may have forgotten to define `render`.',
1269 + 'Warning: RenderTestUndefinedRender(...): No `render` method found on the returned ' +
1270 + 'component instance: you may have forgotten to define `render`.',
1271 ]);
1272 });
1273
1274 // Regression test for accidental breaking change
1275 // https://github.com/facebook/react/issues/13580
1729 - it('should support classes shadowing isReactComponent', () => {
1276 + it('should support classes shadowing isReactComponent', async () => {
1277 class Shadow extends React.Component {
1278 isReactComponent() {}
1279 render() {
@@ -1734,19 +1281,24 @@ describe('ReactCompositeComponent', () => {
1281 }
1282 }
1283 const container = document.createElement('div');
1737 - ReactDOM.render(<Shadow />, container);
1284 + const root = ReactDOMClient.createRoot(container);
1285 + await act(() => {
1286 + root.render(<Shadow />);
1287 + });
1288 expect(container.firstChild.tagName).toBe('DIV');
1289 });
1290
1741 - it('should not warn on updating function component from componentWillMount', () => {
1742 - let _setState;
1291 + it('should not warn on updating function component from componentWillMount', async () => {
1292 + let setState;
1293 + let ref;
1294 function A() {
1744 - _setState = React.useState()[1];
1745 - return null;
1295 + const [state, _setState] = React.useState(null);
1296 + setState = _setState;
1297 + return <div ref={r => (ref = r)}>{state}</div>;
1298 }
1299 class B extends React.Component {
1300 UNSAFE_componentWillMount() {
1749 - _setState({});
1301 + setState(1);
1302 }
1303 render() {
1304 return null;
@@ -1761,18 +1313,25 @@ describe('ReactCompositeComponent', () => {
1313 );
1314 }
1315 const container = document.createElement('div');
1764 - ReactDOM.render(<Parent />, container);
1316 + const root = ReactDOMClient.createRoot(container);
1317 + await act(() => {
1318 + root.render(<Parent />);
1319 + });
1320 +
1321 + expect(ref.textContent).toBe('1');
1322 });
1323
1767 - it('should not warn on updating function component from componentWillUpdate', () => {
1768 - let _setState;
1324 + it('should not warn on updating function component from componentWillUpdate', async () => {
1325 + let setState;
1326 + let ref;
1327 function A() {
1770 - _setState = React.useState()[1];
1771 - return null;
1328 + const [state, _setState] = React.useState();
1329 + setState = _setState;
1330 + return <div ref={r => (ref = r)}>{state}</div>;
1331 }
1332 class B extends React.Component {
1333 UNSAFE_componentWillUpdate() {
1775 - _setState({});
1334 + setState(1);
1335 }
1336 render() {
1337 return null;
@@ -1787,19 +1346,29 @@ describe('ReactCompositeComponent', () => {
1346 );
1347 }
1348 const container = document.createElement('div');
1790 - ReactDOM.render(<Parent />, container);
1791 - ReactDOM.render(<Parent />, container);
1349 + const root = ReactDOMClient.createRoot(container);
1350 + await act(() => {
1351 + root.render(<Parent />);
1352 + });
1353 + await act(() => {
1354 + root.render(<Parent />);
1355 + });
1356 +
1357 + expect(ref.textContent).toBe('1');
1358 });
1359
1794 - it('should not warn on updating function component from componentWillReceiveProps', () => {
1795 - let _setState;
1360 + it('should not warn on updating function component from componentWillReceiveProps', async () => {
1361 + let setState;
1362 + let ref;
1363 function A() {
1797 - _setState = React.useState()[1];
1798 - return null;
1364 + const [state, _setState] = React.useState();
1365 + setState = _setState;
1366 + return <div ref={r => (ref = r)}>{state}</div>;
1367 }
1368 +
1369 class B extends React.Component {
1370 UNSAFE_componentWillReceiveProps() {
1802 - _setState({});
1371 + setState(1);
1372 }
1373 render() {
1374 return null;
@@ -1814,19 +1383,29 @@ describe('ReactCompositeComponent', () => {
1383 );
1384 }
1385 const container = document.createElement('div');
1817 - ReactDOM.render(<Parent />, container);
1818 - ReactDOM.render(<Parent />, container);
1386 + const root = ReactDOMClient.createRoot(container);
1387 + await act(() => {
1388 + root.render(<Parent />);
1389 + });
1390 + await act(() => {
1391 + root.render(<Parent />);
1392 + });
1393 +
1394 + expect(ref.textContent).toBe('1');
1395 });
1396
1397 it('should warn on updating function component from render', () => {
1822 - let _setState;
1398 + let setState;
1399 + let ref;
1400 function A() {
1824 - _setState = React.useState()[1];
1825 - return null;
1401 + const [state, _setState] = React.useState(0);
1402 + setState = _setState;
1403 + return <div ref={r => (ref = r)}>{state}</div>;
1404 }
1405 +
1406 class B extends React.Component {
1407 render() {
1829 - _setState({});
1408 + setState(c => c + 1);
1409 return null;
1410 }
1411 }
@@ -1839,12 +1418,24 @@ describe('ReactCompositeComponent', () => {
1418 );
1419 }
1420 const container = document.createElement('div');
1421 + const root = ReactDOMClient.createRoot(container);
1422 expect(() => {
1843 - ReactDOM.render(<Parent />, container);
1423 + ReactDOM.flushSync(() => {
1424 + root.render(<Parent />);
1425 + });
1426 }).toErrorDev(
1427 'Cannot update a component (`A`) while rendering a different component (`B`)',
1428 );
1429 +
1430 + // We error, but still update the state.
1431 + expect(ref.textContent).toBe('1');
1432 +
1433 // Dedupe.
1848 - ReactDOM.render(<Parent />, container);
1434 + ReactDOM.flushSync(() => {
1435 + root.render(<Parent />);
1436 + });
1437 +
1438 + // We error, but still update the state.
1439 + expect(ref.textContent).toBe('2');
1440 });
1441 });
packages/react-dom/src/__tests__/ReactLegacyCompositeComponent-test.js new
+800
@@ -0,0 +1,800 @@
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 ReactTestUtils;
15 +let PropTypes;
16 +
17 +describe('ReactLegacyCompositeComponent', () => {
18 + beforeEach(() => {
19 + jest.resetModules();
20 + React = require('react');
21 + ReactDOM = require('react-dom');
22 + ReactTestUtils = require('react-dom/test-utils');
23 + PropTypes = require('prop-types');
24 + });
25 +
26 + it('should warn about `setState` in render in legacy mode', () => {
27 + const container = document.createElement('div');
28 +
29 + let renderedState = -1;
30 + let renderPasses = 0;
31 +
32 + class Component extends React.Component {
33 + state = {value: 0};
34 +
35 + render() {
36 + renderPasses++;
37 + renderedState = this.state.value;
38 + if (this.state.value === 0) {
39 + this.setState({value: 1});
40 + }
41 + return <div />;
42 + }
43 + }
44 +
45 + let instance;
46 +
47 + expect(() => {
48 + instance = ReactDOM.render(<Component />, container);
49 + }).toErrorDev(
50 + 'Cannot update during an existing state transition (such as within ' +
51 + '`render`). Render methods should be a pure function of props and state.',
52 + );
53 +
54 + // The setState call is queued and then executed as a second pass. This
55 + // behavior is undefined though so we're free to change it to suit the
56 + // implementation details.
57 + expect(renderPasses).toBe(2);
58 + expect(renderedState).toBe(1);
59 + expect(instance.state.value).toBe(1);
60 +
61 + // Forcing a rerender anywhere will cause the update to happen.
62 + const instance2 = ReactDOM.render(<Component prop={123} />, container);
63 + expect(instance).toBe(instance2);
64 + expect(renderedState).toBe(1);
65 + expect(instance2.state.value).toBe(1);
66 +
67 + // Test deduplication; (no additional warnings are expected).
68 + ReactDOM.unmountComponentAtNode(container);
69 + ReactDOM.render(<Component prop={123} />, container);
70 + });
71 +
72 + // @gate !disableLegacyContext
73 + it('should pass context to children when not owner', () => {
74 + class Parent extends React.Component {
75 + render() {
76 + return (
77 + <Child>
78 + <Grandchild />
79 + </Child>
80 + );
81 + }
82 + }
83 +
84 + class Child extends React.Component {
85 + static childContextTypes = {
86 + foo: PropTypes.string,
87 + };
88 +
89 + getChildContext() {
90 + return {
91 + foo: 'bar',
92 + };
93 + }
94 +
95 + render() {
96 + return React.Children.only(this.props.children);
97 + }
98 + }
99 +
100 + class Grandchild extends React.Component {
101 + static contextTypes = {
102 + foo: PropTypes.string,
103 + };
104 +
105 + render() {
106 + return <div>{this.context.foo}</div>;
107 + }
108 + }
109 +
110 + const component = ReactTestUtils.renderIntoDocument(<Parent />);
111 + expect(ReactDOM.findDOMNode(component).innerHTML).toBe('bar');
112 + });
113 +
114 + // @gate !disableLegacyContext
115 + it('should pass context when re-rendered for static child', () => {
116 + let parentInstance = null;
117 + let childInstance = null;
118 +
119 + class Parent extends React.Component {
120 + static childContextTypes = {
121 + foo: PropTypes.string,
122 + flag: PropTypes.bool,
123 + };
124 +
125 + state = {
126 + flag: false,
127 + };
128 +
129 + getChildContext() {
130 + return {
131 + foo: 'bar',
132 + flag: this.state.flag,
133 + };
134 + }
135 +
136 + render() {
137 + return React.Children.only(this.props.children);
138 + }
139 + }
140 +
141 + class Middle extends React.Component {
142 + render() {
143 + return this.props.children;
144 + }
145 + }
146 +
147 + class Child extends React.Component {
148 + static contextTypes = {
149 + foo: PropTypes.string,
150 + flag: PropTypes.bool,
151 + };
152 +
153 + render() {
154 + childInstance = this;
155 + return <span>Child</span>;
156 + }
157 + }
158 +
159 + parentInstance = ReactTestUtils.renderIntoDocument(
160 + <Parent>
161 + <Middle>
162 + <Child />
163 + </Middle>
164 + </Parent>,
165 + );
166 +
167 + expect(parentInstance.state.flag).toBe(false);
168 + expect(childInstance.context).toEqual({foo: 'bar', flag: false});
169 +
170 + parentInstance.setState({flag: true});
171 + expect(parentInstance.state.flag).toBe(true);
172 + expect(childInstance.context).toEqual({foo: 'bar', flag: true});
173 + });
174 +
175 + // @gate !disableLegacyContext
176 + it('should pass context when re-rendered for static child within a composite component', () => {
177 + class Parent extends React.Component {
178 + static childContextTypes = {
179 + flag: PropTypes.bool,
180 + };
181 +
182 + state = {
183 + flag: true,
184 + };
185 +
186 + getChildContext() {
187 + return {
188 + flag: this.state.flag,
189 + };
190 + }
191 +
192 + render() {
193 + return <div>{this.props.children}</div>;
194 + }
195 + }
196 +
197 + class Child extends React.Component {
198 + static contextTypes = {
199 + flag: PropTypes.bool,
200 + };
201 +
202 + render() {
203 + return <div />;
204 + }
205 + }
206 +
207 + class Wrapper extends React.Component {
208 + parentRef = React.createRef();
209 + childRef = React.createRef();
210 +
211 + render() {
212 + return (
213 + <Parent ref={this.parentRef}>
214 + <Child ref={this.childRef} />
215 + </Parent>
216 + );
217 + }
218 + }
219 +
220 + const wrapper = ReactTestUtils.renderIntoDocument(<Wrapper />);
221 +
222 + expect(wrapper.parentRef.current.state.flag).toEqual(true);
223 + expect(wrapper.childRef.current.context).toEqual({flag: true});
224 +
225 + // We update <Parent /> while <Child /> is still a static prop relative to this update
226 + wrapper.parentRef.current.setState({flag: false});
227 +
228 + expect(wrapper.parentRef.current.state.flag).toEqual(false);
229 + expect(wrapper.childRef.current.context).toEqual({flag: false});
230 + });
231 +
232 + // @gate !disableLegacyContext
233 + it('should pass context transitively', () => {
234 + let childInstance = null;
235 + let grandchildInstance = null;
236 +
237 + class Parent extends React.Component {
238 + static childContextTypes = {
239 + foo: PropTypes.string,
240 + depth: PropTypes.number,
241 + };
242 +
243 + getChildContext() {
244 + return {
245 + foo: 'bar',
246 + depth: 0,
247 + };
248 + }
249 +
250 + render() {
251 + return <Child />;
252 + }
253 + }
254 +
255 + class Child extends React.Component {
256 + static contextTypes = {
257 + foo: PropTypes.string,
258 + depth: PropTypes.number,
259 + };
260 +
261 + static childContextTypes = {
262 + depth: PropTypes.number,
263 + };
264 +
265 + getChildContext() {
266 + return {
267 + depth: this.context.depth + 1,
268 + };
269 + }
270 +
271 + render() {
272 + childInstance = this;
273 + return <Grandchild />;
274 + }
275 + }
276 +
277 + class Grandchild extends React.Component {
278 + static contextTypes = {
279 + foo: PropTypes.string,
280 + depth: PropTypes.number,
281 + };
282 +
283 + render() {
284 + grandchildInstance = this;
285 + return <div />;
286 + }
287 + }
288 +
289 + ReactTestUtils.renderIntoDocument(<Parent />);
290 + expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
291 + expect(grandchildInstance.context).toEqual({foo: 'bar', depth: 1});
292 + });
293 +
294 + // @gate !disableLegacyContext
295 + it('should pass context when re-rendered', () => {
296 + let parentInstance = null;
297 + let childInstance = null;
298 +
299 + class Parent extends React.Component {
300 + static childContextTypes = {
301 + foo: PropTypes.string,
302 + depth: PropTypes.number,
303 + };
304 +
305 + state = {
306 + flag: false,
307 + };
308 +
309 + getChildContext() {
310 + return {
311 + foo: 'bar',
312 + depth: 0,
313 + };
314 + }
315 +
316 + render() {
317 + let output = <Child />;
318 + if (!this.state.flag) {
319 + output = <span>Child</span>;
320 + }
321 + return output;
322 + }
323 + }
324 +
325 + class Child extends React.Component {
326 + static contextTypes = {
327 + foo: PropTypes.string,
328 + depth: PropTypes.number,
329 + };
330 +
331 + render() {
332 + childInstance = this;
333 + return <span>Child</span>;
334 + }
335 + }
336 +
337 + parentInstance = ReactTestUtils.renderIntoDocument(<Parent />);
338 + expect(childInstance).toBeNull();
339 +
340 + expect(parentInstance.state.flag).toBe(false);
341 + ReactDOM.unstable_batchedUpdates(function () {
342 + parentInstance.setState({flag: true});
343 + });
344 + expect(parentInstance.state.flag).toBe(true);
345 +
346 + expect(childInstance.context).toEqual({foo: 'bar', depth: 0});
347 + });
348 +
349 + // @gate !disableLegacyContext
350 + it('unmasked context propagates through updates', () => {
351 + class Leaf extends React.Component {
352 + static contextTypes = {
353 + foo: PropTypes.string.isRequired,
354 + };
355 +
356 + UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
357 + expect('foo' in nextContext).toBe(true);
358 + }
359 +
360 + shouldComponentUpdate(nextProps, nextState, nextContext) {
361 + expect('foo' in nextContext).toBe(true);
362 + return true;
363 + }
364 +
365 + render() {
366 + return <span>{this.context.foo}</span>;
367 + }
368 + }
369 +
370 + class Intermediary extends React.Component {
371 + UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
372 + expect('foo' in nextContext).toBe(false);
373 + }
374 +
375 + shouldComponentUpdate(nextProps, nextState, nextContext) {
376 + expect('foo' in nextContext).toBe(false);
377 + return true;
378 + }
379 +
380 + render() {
381 + return <Leaf />;
382 + }
383 + }
384 +
385 + class Parent extends React.Component {
386 + static childContextTypes = {
387 + foo: PropTypes.string,
388 + };
389 +
390 + getChildContext() {
391 + return {
392 + foo: this.props.cntxt,
393 + };
394 + }
395 +
396 + render() {
397 + return <Intermediary />;
398 + }
399 + }
400 +
401 + const div = document.createElement('div');
402 + ReactDOM.render(<Parent cntxt="noise" />, div);
403 + expect(div.children[0].innerHTML).toBe('noise');
404 + div.children[0].innerHTML = 'aliens';
405 + div.children[0].id = 'aliens';
406 + expect(div.children[0].innerHTML).toBe('aliens');
407 + expect(div.children[0].id).toBe('aliens');
408 + ReactDOM.render(<Parent cntxt="bar" />, div);
409 + expect(div.children[0].innerHTML).toBe('bar');
410 + expect(div.children[0].id).toBe('aliens');
411 + });
412 +
413 + // @gate !disableLegacyContext
414 + it('should trigger componentWillReceiveProps for context changes', () => {
415 + let contextChanges = 0;
416 + let propChanges = 0;
417 +
418 + class GrandChild extends React.Component {
419 + static contextTypes = {
420 + foo: PropTypes.string.isRequired,
421 + };
422 +
423 + UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
424 + expect('foo' in nextContext).toBe(true);
425 +
426 + if (nextProps !== this.props) {
427 + propChanges++;
428 + }
429 +
430 + if (nextContext !== this.context) {
431 + contextChanges++;
432 + }
433 + }
434 +
435 + render() {
436 + return <span className="grand-child">{this.props.children}</span>;
437 + }
438 + }
439 +
440 + class ChildWithContext extends React.Component {
441 + static contextTypes = {
442 + foo: PropTypes.string.isRequired,
443 + };
444 +
445 + UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
446 + expect('foo' in nextContext).toBe(true);
447 +
448 + if (nextProps !== this.props) {
449 + propChanges++;
450 + }
451 +
452 + if (nextContext !== this.context) {
453 + contextChanges++;
454 + }
455 + }
456 +
457 + render() {
458 + return <div className="child-with">{this.props.children}</div>;
459 + }
460 + }
461 +
462 + class ChildWithoutContext extends React.Component {
463 + UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
464 + expect('foo' in nextContext).toBe(false);
465 +
466 + if (nextProps !== this.props) {
467 + propChanges++;
468 + }
469 +
470 + if (nextContext !== this.context) {
471 + contextChanges++;
472 + }
473 + }
474 +
475 + render() {
476 + return <div className="child-without">{this.props.children}</div>;
477 + }
478 + }
479 +
480 + class Parent extends React.Component {
481 + static childContextTypes = {
482 + foo: PropTypes.string,
483 + };
484 +
485 + state = {
486 + foo: 'abc',
487 + };
488 +
489 + getChildContext() {
490 + return {
491 + foo: this.state.foo,
492 + };
493 + }
494 +
495 + render() {
496 + return <div className="parent">{this.props.children}</div>;
497 + }
498 + }
499 +
500 + const div = document.createElement('div');
501 +
502 + let parentInstance = null;
503 + ReactDOM.render(
504 + <Parent ref={inst => (parentInstance = inst)}>
505 + <ChildWithoutContext>
506 + A1
507 + <GrandChild>A2</GrandChild>
508 + </ChildWithoutContext>
509 +
510 + <ChildWithContext>
511 + B1
512 + <GrandChild>B2</GrandChild>
513 + </ChildWithContext>
514 + </Parent>,
515 + div,
516 + );
517 +
518 + parentInstance.setState({
519 + foo: 'def',
520 + });
521 +
522 + expect(propChanges).toBe(0);
523 + expect(contextChanges).toBe(3); // ChildWithContext, GrandChild x 2
524 + });
525 +
526 + it('only renders once if updated in componentWillReceiveProps in legacy mode', () => {
527 + let renders = 0;
528 +
529 + class Component extends React.Component {
530 + state = {updated: false};
531 +
532 + UNSAFE_componentWillReceiveProps(props) {
533 + expect(props.update).toBe(1);
534 + expect(renders).toBe(1);
535 + this.setState({updated: true});
536 + expect(renders).toBe(1);
537 + }
538 +
539 + render() {
540 + renders++;
541 + return <div />;
542 + }
543 + }
544 +
545 + const container = document.createElement('div');
546 + const instance = ReactDOM.render(<Component update={0} />, container);
547 + expect(renders).toBe(1);
548 + expect(instance.state.updated).toBe(false);
549 + ReactDOM.render(<Component update={1} />, container);
550 + expect(renders).toBe(2);
551 + expect(instance.state.updated).toBe(true);
552 + });
553 +
554 + it('only renders once if updated in componentWillReceiveProps when batching in legacy mode', () => {
555 + let renders = 0;
556 +
557 + class Component extends React.Component {
558 + state = {updated: false};
559 +
560 + UNSAFE_componentWillReceiveProps(props) {
561 + expect(props.update).toBe(1);
562 + expect(renders).toBe(1);
563 + this.setState({updated: true});
564 + expect(renders).toBe(1);
565 + }
566 +
567 + render() {
568 + renders++;
569 + return <div />;
570 + }
571 + }
572 +
573 + const container = document.createElement('div');
574 + const instance = ReactDOM.render(<Component update={0} />, container);
575 + expect(renders).toBe(1);
576 + expect(instance.state.updated).toBe(false);
577 + ReactDOM.unstable_batchedUpdates(() => {
578 + ReactDOM.render(<Component update={1} />, container);
579 + });
580 + expect(renders).toBe(2);
581 + expect(instance.state.updated).toBe(true);
582 + });
583 +
584 + it('should update refs if shouldComponentUpdate gives false in legacy mode', () => {
585 + class Static extends React.Component {
586 + shouldComponentUpdate() {
587 + return false;
588 + }
589 +
590 + render() {
591 + return <div>{this.props.children}</div>;
592 + }
593 + }
594 +
595 + class Component extends React.Component {
596 + static0Ref = React.createRef();
597 + static1Ref = React.createRef();
598 +
599 + render() {
600 + if (this.props.flipped) {
601 + return (
602 + <div>
603 + <Static ref={this.static0Ref} key="B">
604 + B (ignored)
605 + </Static>
606 + <Static ref={this.static1Ref} key="A">
607 + A (ignored)
608 + </Static>
609 + </div>
610 + );
611 + } else {
612 + return (
613 + <div>
614 + <Static ref={this.static0Ref} key="A">
615 + A
616 + </Static>
617 + <Static ref={this.static1Ref} key="B">
618 + B
619 + </Static>
620 + </div>
621 + );
622 + }
623 + }
624 + }
625 +
626 + const container = document.createElement('div');
627 + const comp = ReactDOM.render(<Component flipped={false} />, container);
628 + expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('A');
629 + expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('B');
630 +
631 + // When flipping the order, the refs should update even though the actual
632 + // contents do not
633 + ReactDOM.render(<Component flipped={true} />, container);
634 + expect(ReactDOM.findDOMNode(comp.static0Ref.current).textContent).toBe('B');
635 + expect(ReactDOM.findDOMNode(comp.static1Ref.current).textContent).toBe('A');
636 + });
637 +
638 + it('should allow access to findDOMNode in componentWillUnmount in legacy mode', () => {
639 + let a = null;
640 + let b = null;
641 +
642 + class Component extends React.Component {
643 + componentDidMount() {
644 + a = ReactDOM.findDOMNode(this);
645 + expect(a).not.toBe(null);
646 + }
647 +
648 + componentWillUnmount() {
649 + b = ReactDOM.findDOMNode(this);
650 + expect(b).not.toBe(null);
651 + }
652 +
653 + render() {
654 + return <div />;
655 + }
656 + }
657 +
658 + const container = document.createElement('div');
659 + expect(a).toBe(container.firstChild);
660 + ReactDOM.render(<Component />, container);
661 + ReactDOM.unmountComponentAtNode(container);
662 + expect(a).toBe(b);
663 + });
664 +
665 + // @gate !disableLegacyContext || !__DEV__
666 + it('context should be passed down from the parent', () => {
667 + class Parent extends React.Component {
668 + static childContextTypes = {
669 + foo: PropTypes.string,
670 + };
671 +
672 + getChildContext() {
673 + return {
674 + foo: 'bar',
675 + };
676 + }
677 +
678 + render() {
679 + return <div>{this.props.children}</div>;
680 + }
681 + }
682 +
683 + class Component extends React.Component {
684 + static contextTypes = {
685 + foo: PropTypes.string.isRequired,
686 + };
687 +
688 + render() {
689 + return <div />;
690 + }
691 + }
692 +
693 + const div = document.createElement('div');
694 + ReactDOM.render(
695 + <Parent>
696 + <Component />
697 + </Parent>,
698 + div,
699 + );
700 + });
701 +
702 + it('should replace state in legacy mode', () => {
703 + class Moo extends React.Component {
704 + state = {x: 1};
705 + render() {
706 + return <div />;
707 + }
708 + }
709 +
710 + const moo = ReactTestUtils.renderIntoDocument(<Moo />);
711 + // No longer a public API, but we can test that it works internally by
712 + // reaching into the updater.
713 + moo.updater.enqueueReplaceState(moo, {y: 2});
714 + expect('x' in moo.state).toBe(false);
715 + expect(moo.state.y).toBe(2);
716 + });
717 +
718 + it('should support objects with prototypes as state in legacy mode', () => {
719 + const NotActuallyImmutable = function (str) {
720 + this.str = str;
721 + };
722 + NotActuallyImmutable.prototype.amIImmutable = function () {
723 + return true;
724 + };
725 + class Moo extends React.Component {
726 + state = new NotActuallyImmutable('first');
727 + // No longer a public API, but we can test that it works internally by
728 + // reaching into the updater.
729 + _replaceState = update => this.updater.enqueueReplaceState(this, update);
730 + render() {
731 + return <div />;
732 + }
733 + }
734 +
735 + const moo = ReactTestUtils.renderIntoDocument(<Moo />);
736 + expect(moo.state.str).toBe('first');
737 + expect(moo.state.amIImmutable()).toBe(true);
738 +
739 + const secondState = new NotActuallyImmutable('second');
740 + moo._replaceState(secondState);
741 + expect(moo.state.str).toBe('second');
742 + expect(moo.state.amIImmutable()).toBe(true);
743 + expect(moo.state).toBe(secondState);
744 +
745 + moo.setState({str: 'third'});
746 + expect(moo.state.str).toBe('third');
747 + // Here we lose the prototype.
748 + expect(moo.state.amIImmutable).toBe(undefined);
749 +
750 + // When more than one state update is enqueued, we have the same behavior
751 + const fifthState = new NotActuallyImmutable('fifth');
752 + ReactDOM.unstable_batchedUpdates(function () {
753 + moo.setState({str: 'fourth'});
754 + moo._replaceState(fifthState);
755 + });
756 + expect(moo.state).toBe(fifthState);
757 +
758 + // When more than one state update is enqueued, we have the same behavior
759 + const sixthState = new NotActuallyImmutable('sixth');
760 + ReactDOM.unstable_batchedUpdates(function () {
761 + moo._replaceState(sixthState);
762 + moo.setState({str: 'seventh'});
763 + });
764 + expect(moo.state.str).toBe('seventh');
765 + expect(moo.state.amIImmutable).toBe(undefined);
766 + });
767 +
768 + it('should not warn about unmounting during unmounting in legacy mode', () => {
769 + const container = document.createElement('div');
770 + const layer = document.createElement('div');
771 +
772 + class Component extends React.Component {
773 + componentDidMount() {
774 + ReactDOM.render(<div />, layer);
775 + }
776 +
777 + componentWillUnmount() {
778 + ReactDOM.unmountComponentAtNode(layer);
779 + }
780 +
781 + render() {
782 + return <div />;
783 + }
784 + }
785 +
786 + class Outer extends React.Component {
787 + render() {
788 + return <div>{this.props.children}</div>;
789 + }
790 + }
791 +
792 + ReactDOM.render(
793 + <Outer>
794 + <Component />
795 + </Outer>,
796 + container,
797 + );
798 + ReactDOM.render(<Outer />, container);
799 + });
800 +});