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