main
js 660 lines 21.2 KB
Raw
1 /**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 *
7 * @emails react-core
8 */
9
10 // This test doesn't really have a good home yet. I'm leaving it here since this
11 // behavior belongs to the old propTypes system yet is currently implemented
12 // in the core ReactCompositeComponent. It should technically live in core's
13 // test suite but I'll leave it here to indicate that this is an issue that
14 // needs to be fixed.
15
16 'use strict';
17
18 let PropTypes;
19 let React;
20 let ReactDOMClient;
21 let act;
22 let assertConsoleErrorDev;
23
24 describe('ReactContextValidator', () => {
25 beforeEach(() => {
26 jest.resetModules();
27
28 PropTypes = require('prop-types');
29 React = require('react');
30 ReactDOMClient = require('react-dom/client');
31 ({act, assertConsoleErrorDev} = require('internal-test-utils'));
32 });
33
34 // TODO: This behavior creates a runtime dependency on propTypes. We should
35 // ensure that this is not required for ES6 classes with Flow.
36
37 // @gate !disableLegacyContext
38 it('should filter out context not in contextTypes', async () => {
39 class Component extends React.Component {
40 render() {
41 return <div />;
42 }
43 }
44 Component.contextTypes = {
45 foo: PropTypes.string,
46 };
47
48 class ComponentInFooBarContext extends React.Component {
49 childRef = React.createRef();
50
51 getChildContext() {
52 return {
53 foo: 'abc',
54 bar: 123,
55 };
56 }
57
58 render() {
59 return <Component ref={this.childRef} />;
60 }
61 }
62 ComponentInFooBarContext.childContextTypes = {
63 foo: PropTypes.string,
64 bar: PropTypes.number,
65 };
66
67 let instance;
68 const container = document.createElement('div');
69 const root = ReactDOMClient.createRoot(container);
70 await act(() => {
71 root.render(
72 <ComponentInFooBarContext ref={current => (instance = current)} />,
73 );
74 });
75 assertConsoleErrorDev([
76 'ComponentInFooBarContext uses the legacy childContextTypes API which will soon be removed. ' +
77 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
78 ' in ComponentInFooBarContext (at **)',
79 'Component uses the legacy contextTypes API which will soon be removed. ' +
80 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
81 ' in ComponentInFooBarContext (at **)',
82 ]);
83 expect(instance.childRef.current.context).toEqual({foo: 'abc'});
84 });
85
86 // @gate !disableLegacyContext
87 it('should pass next context to lifecycles', async () => {
88 let componentDidMountContext;
89 let componentDidUpdateContext;
90 let componentWillReceivePropsContext;
91 let componentWillReceivePropsNextContext;
92 let componentWillUpdateContext;
93 let componentWillUpdateNextContext;
94 let constructorContext;
95 let renderContext;
96 let shouldComponentUpdateContext;
97 let shouldComponentUpdateNextContext;
98
99 class Parent extends React.Component {
100 getChildContext() {
101 return {
102 foo: this.props.foo,
103 bar: 'bar',
104 };
105 }
106 render() {
107 return <Component />;
108 }
109 }
110 Parent.childContextTypes = {
111 foo: PropTypes.string.isRequired,
112 bar: PropTypes.string.isRequired,
113 };
114
115 class Component extends React.Component {
116 constructor(props, context) {
117 super(props, context);
118 constructorContext = context;
119 }
120 UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
121 componentWillReceivePropsContext = this.context;
122 componentWillReceivePropsNextContext = nextContext;
123 return true;
124 }
125 shouldComponentUpdate(nextProps, nextState, nextContext) {
126 shouldComponentUpdateContext = this.context;
127 shouldComponentUpdateNextContext = nextContext;
128 return true;
129 }
130 UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
131 componentWillUpdateContext = this.context;
132 componentWillUpdateNextContext = nextContext;
133 }
134 render() {
135 renderContext = this.context;
136 return <div />;
137 }
138 componentDidMount() {
139 componentDidMountContext = this.context;
140 }
141 componentDidUpdate() {
142 componentDidUpdateContext = this.context;
143 }
144 }
145 Component.contextTypes = {
146 foo: PropTypes.string,
147 };
148
149 const container = document.createElement('div');
150 const root = ReactDOMClient.createRoot(container);
151 await act(() => {
152 root.render(<Parent foo="abc" />);
153 });
154 assertConsoleErrorDev([
155 'Parent uses the legacy childContextTypes API which will soon be removed. ' +
156 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
157 ' in Parent (at **)',
158 'Component uses the legacy contextTypes API which will soon be removed. ' +
159 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
160 ' in Parent (at **)',
161 ]);
162
163 expect(constructorContext).toEqual({foo: 'abc'});
164 expect(renderContext).toEqual({foo: 'abc'});
165 expect(componentDidMountContext).toEqual({foo: 'abc'});
166 await act(() => {
167 root.render(<Parent foo="def" />);
168 });
169
170 expect(componentWillReceivePropsContext).toEqual({foo: 'abc'});
171 expect(componentWillReceivePropsNextContext).toEqual({foo: 'def'});
172 expect(shouldComponentUpdateContext).toEqual({foo: 'abc'});
173 expect(shouldComponentUpdateNextContext).toEqual({foo: 'def'});
174 expect(componentWillUpdateContext).toEqual({foo: 'abc'});
175 expect(componentWillUpdateNextContext).toEqual({foo: 'def'});
176 expect(renderContext).toEqual({foo: 'def'});
177 expect(componentDidUpdateContext).toEqual({foo: 'def'});
178 });
179
180 // TODO (bvaughn) Remove this test and the associated behavior in the future.
181 // It has only been added in Fiber to match the (unintentional) behavior in Stack.
182 // @gate !disableLegacyContext || !__DEV__
183 it('should warn (but not error) if getChildContext method is missing', async () => {
184 class ComponentA extends React.Component {
185 static childContextTypes = {
186 foo: PropTypes.string.isRequired,
187 };
188 render() {
189 return <div />;
190 }
191 }
192 class ComponentB extends React.Component {
193 static childContextTypes = {
194 foo: PropTypes.string.isRequired,
195 };
196 render() {
197 return <div />;
198 }
199 }
200
201 const root = ReactDOMClient.createRoot(document.createElement('div'));
202 await act(() => {
203 root.render(<ComponentA />);
204 });
205 assertConsoleErrorDev([
206 'ComponentA uses the legacy childContextTypes API which will soon be removed. ' +
207 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
208 ' in ComponentA (at **)',
209 'ComponentA.childContextTypes is specified but there is no getChildContext() method on the instance. ' +
210 'You can either define getChildContext() on ComponentA or remove childContextTypes from it.\n' +
211 ' in ComponentA (at **)',
212 ]);
213
214 // Warnings should be deduped by component type
215 await act(() => {
216 root.render(<ComponentA />);
217 });
218 await act(() => {
219 root.render(<ComponentB />);
220 });
221 assertConsoleErrorDev([
222 'ComponentB uses the legacy childContextTypes API which will soon be removed. ' +
223 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
224 ' in ComponentB (at **)',
225 'ComponentB.childContextTypes is specified but there is no getChildContext() method on the instance. ' +
226 'You can either define getChildContext() on ComponentB or remove childContextTypes from it.\n' +
227 ' in ComponentB (at **)',
228 ]);
229 });
230
231 // TODO (bvaughn) Remove this test and the associated behavior in the future.
232 // It has only been added in Fiber to match the (unintentional) behavior in Stack.
233 // @gate !disableLegacyContext
234 it('should pass parent context if getChildContext method is missing', async () => {
235 class ParentContextProvider extends React.Component {
236 static childContextTypes = {
237 foo: PropTypes.string,
238 };
239 getChildContext() {
240 return {
241 foo: 'FOO',
242 };
243 }
244 render() {
245 return <MiddleMissingContext />;
246 }
247 }
248
249 class MiddleMissingContext extends React.Component {
250 static childContextTypes = {
251 bar: PropTypes.string.isRequired,
252 };
253 render() {
254 return <ChildContextConsumer />;
255 }
256 }
257
258 let childContext;
259 class ChildContextConsumer extends React.Component {
260 render() {
261 childContext = this.context;
262 return <div />;
263 }
264 }
265 ChildContextConsumer.contextTypes = {
266 bar: PropTypes.string.isRequired,
267 foo: PropTypes.string.isRequired,
268 };
269
270 const container = document.createElement('div');
271 const root = ReactDOMClient.createRoot(container);
272 await act(() => {
273 root.render(<ParentContextProvider />);
274 });
275 assertConsoleErrorDev([
276 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
277 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
278 ' in ParentContextProvider (at **)',
279 'MiddleMissingContext uses the legacy childContextTypes API which will soon be removed. ' +
280 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
281 ' in ParentContextProvider (at **)',
282 'MiddleMissingContext.childContextTypes is specified but there is no getChildContext() method on the instance. ' +
283 'You can either define getChildContext() on MiddleMissingContext or remove childContextTypes from it.\n' +
284 ' in ParentContextProvider (at **)',
285 'ChildContextConsumer uses the legacy contextTypes API which will soon be removed. ' +
286 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
287 ' in MiddleMissingContext (at **)\n' +
288 ' in ParentContextProvider (at **)',
289 ]);
290 expect(childContext.bar).toBeUndefined();
291 expect(childContext.foo).toBe('FOO');
292 });
293
294 it('should pass next context to lifecycles on update', async () => {
295 let componentDidMountContext;
296 let componentDidUpdateContext;
297 let componentWillReceivePropsContext;
298 let componentWillReceivePropsNextContext;
299 let componentWillUpdateContext;
300 let componentWillUpdateNextContext;
301 let constructorContext;
302 let renderContext;
303 let shouldComponentUpdateWasCalled = false;
304
305 const Context = React.createContext();
306
307 class Component extends React.Component {
308 static contextType = Context;
309 constructor(props, context) {
310 super(props, context);
311 constructorContext = context;
312 }
313 UNSAFE_componentWillReceiveProps(nextProps, nextContext) {
314 componentWillReceivePropsContext = this.context;
315 componentWillReceivePropsNextContext = nextContext;
316 return true;
317 }
318 shouldComponentUpdate(nextProps, nextState, nextContext) {
319 shouldComponentUpdateWasCalled = true;
320 return true;
321 }
322 UNSAFE_componentWillUpdate(nextProps, nextState, nextContext) {
323 componentWillUpdateContext = this.context;
324 componentWillUpdateNextContext = nextContext;
325 }
326 render() {
327 renderContext = this.context;
328 return <div />;
329 }
330 componentDidMount() {
331 componentDidMountContext = this.context;
332 }
333 componentDidUpdate() {
334 componentDidUpdateContext = this.context;
335 }
336 }
337
338 const firstContext = {foo: 123};
339 const secondContext = {bar: 456};
340
341 const container = document.createElement('div');
342 const root = ReactDOMClient.createRoot(container);
343 await act(() => {
344 root.render(
345 <Context.Provider value={firstContext}>
346 <Component />
347 </Context.Provider>,
348 );
349 });
350
351 expect(constructorContext).toBe(firstContext);
352 expect(renderContext).toBe(firstContext);
353 expect(componentDidMountContext).toBe(firstContext);
354 await act(() => {
355 root.render(
356 <Context.Provider value={secondContext}>
357 <Component />
358 </Context.Provider>,
359 );
360 });
361
362 expect(componentWillReceivePropsContext).toBe(firstContext);
363 expect(componentWillReceivePropsNextContext).toBe(secondContext);
364 expect(componentWillUpdateContext).toBe(firstContext);
365 expect(componentWillUpdateNextContext).toBe(secondContext);
366 expect(renderContext).toBe(secondContext);
367 expect(componentDidUpdateContext).toBe(secondContext);
368 expect(shouldComponentUpdateWasCalled).toBe(true);
369 });
370
371 it('should re-render PureComponents when context Provider updates', async () => {
372 let renderedContext;
373
374 const Context = React.createContext();
375
376 class Component extends React.PureComponent {
377 static contextType = Context;
378 render() {
379 renderedContext = this.context;
380 return <div />;
381 }
382 }
383
384 const firstContext = {foo: 123};
385 const secondContext = {bar: 456};
386
387 const container = document.createElement('div');
388 const root = ReactDOMClient.createRoot(container);
389 await act(() => {
390 root.render(
391 <Context.Provider value={firstContext}>
392 <Component />
393 </Context.Provider>,
394 );
395 });
396
397 expect(renderedContext).toBe(firstContext);
398 await act(() => {
399 root.render(
400 <Context.Provider value={secondContext}>
401 <Component />
402 </Context.Provider>,
403 );
404 });
405
406 expect(renderedContext).toBe(secondContext);
407 });
408
409 // @gate !disableLegacyContext || !__DEV__
410 it('should warn if both contextType and contextTypes are defined', async () => {
411 const Context = React.createContext();
412
413 class ParentContextProvider extends React.Component {
414 static childContextTypes = {
415 foo: PropTypes.string,
416 };
417 getChildContext() {
418 return {
419 foo: 'FOO',
420 };
421 }
422 render() {
423 return this.props.children;
424 }
425 }
426
427 class ComponentA extends React.Component {
428 static contextTypes = {
429 foo: PropTypes.string.isRequired,
430 };
431 static contextType = Context;
432 render() {
433 return <div />;
434 }
435 }
436 class ComponentB extends React.Component {
437 static contextTypes = {
438 foo: PropTypes.string.isRequired,
439 };
440 static contextType = Context;
441 render() {
442 return <div />;
443 }
444 }
445
446 const root = ReactDOMClient.createRoot(document.createElement('div'));
447 await act(() => {
448 root.render(
449 <ParentContextProvider>
450 <ComponentA />
451 </ParentContextProvider>,
452 );
453 });
454
455 assertConsoleErrorDev([
456 'ParentContextProvider uses the legacy childContextTypes API which will soon be removed. ' +
457 'Use React.createContext() instead. (https://react.dev/link/legacy-context)\n' +
458 ' in ParentContextProvider (at **)',
459 'ComponentA declares both contextTypes and contextType static properties. ' +
460 'The legacy contextTypes property will be ignored.\n' +
461 ' in ComponentA (at **)',
462 'ComponentA uses the legacy contextTypes API which will soon be removed. ' +
463 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
464 ' in ComponentA (at **)',
465 ]);
466
467 // Warnings should be deduped by component type
468 await act(() => {
469 root.render(
470 <ParentContextProvider>
471 <ComponentA />
472 </ParentContextProvider>,
473 );
474 });
475
476 await act(() => {
477 root.render(
478 <ParentContextProvider>
479 <ComponentB />
480 </ParentContextProvider>,
481 );
482 });
483 assertConsoleErrorDev([
484 'ComponentB declares both contextTypes and contextType static properties. ' +
485 'The legacy contextTypes property will be ignored.\n' +
486 ' in ComponentB (at **)',
487 'ComponentB uses the legacy contextTypes API which will soon be removed. ' +
488 'Use React.createContext() with static contextType instead. (https://react.dev/link/legacy-context)\n' +
489 ' in ComponentB (at **)',
490 ]);
491 });
492
493 it('should warn if an invalid contextType is defined', async () => {
494 const Context = React.createContext();
495 class ComponentA extends React.Component {
496 static contextType = Context.Consumer;
497 render() {
498 return <div />;
499 }
500 }
501
502 const root = ReactDOMClient.createRoot(document.createElement('div'));
503 await act(() => {
504 root.render(<ComponentA />);
505 });
506 assertConsoleErrorDev([
507 'ComponentA defines an invalid contextType. ' +
508 'contextType should point to the Context object returned by React.createContext(). ' +
509 'Did you accidentally pass the Context.Consumer instead?\n' +
510 ' in ComponentA (at **)',
511 ]);
512
513 await act(() => {
514 root.render(<ComponentA />);
515 });
516
517 class ComponentB extends React.Component {
518 static contextType = Context.Provider;
519 render() {
520 return <div />;
521 }
522 }
523 await act(() => {
524 root.render(<ComponentB />);
525 });
526 });
527
528 it('should not warn when class contextType is null', async () => {
529 class Foo extends React.Component {
530 static contextType = null; // Handy for conditional declaration
531 render() {
532 return this.context.hello.world;
533 }
534 }
535 await expect(async () => {
536 const container = document.createElement('div');
537 const root = ReactDOMClient.createRoot(container);
538 await act(() => {
539 root.render(<Foo />);
540 });
541 }).rejects.toThrow("Cannot read properties of undefined (reading 'world')");
542 });
543
544 it('should warn when class contextType is undefined', async () => {
545 class Foo extends React.Component {
546 // This commonly happens with circular deps
547 // https://github.com/facebook/react/issues/13969
548 static contextType = undefined;
549 render() {
550 return this.context.hello.world;
551 }
552 }
553
554 await expect(async () => {
555 const container = document.createElement('div');
556 const root = ReactDOMClient.createRoot(container);
557 await act(() => {
558 root.render(<Foo />);
559 });
560 }).rejects.toThrow("Cannot read properties of undefined (reading 'world')");
561
562 assertConsoleErrorDev([
563 'Foo defines an invalid contextType. ' +
564 'contextType should point to the Context object returned by React.createContext(). ' +
565 'However, it is set to undefined. ' +
566 'This can be caused by a typo or by mixing up named and default imports. ' +
567 'This can also happen due to a circular dependency, ' +
568 'so try moving the createContext() call to a separate file.\n' +
569 ' in Foo (at **)',
570 ]);
571 });
572
573 it('should warn when class contextType is an object', async () => {
574 class Foo extends React.Component {
575 // Can happen due to a typo
576 static contextType = {
577 x: 42,
578 y: 'hello',
579 };
580 render() {
581 return this.context.hello.world;
582 }
583 }
584
585 await expect(async () => {
586 const container = document.createElement('div');
587 const root = ReactDOMClient.createRoot(container);
588 await act(() => {
589 root.render(<Foo />);
590 });
591 }).rejects.toThrow("Cannot read properties of undefined (reading 'hello')");
592
593 assertConsoleErrorDev([
594 'Foo defines an invalid contextType. ' +
595 'contextType should point to the Context object returned by React.createContext(). ' +
596 'However, it is set to an object with keys {x, y}.\n' +
597 ' in Foo (at **)',
598 ]);
599 });
600
601 it('should warn when class contextType is a primitive', async () => {
602 class Foo extends React.Component {
603 static contextType = 'foo';
604 render() {
605 return this.context.hello.world;
606 }
607 }
608
609 await expect(async () => {
610 const container = document.createElement('div');
611 const root = ReactDOMClient.createRoot(container);
612 await act(() => {
613 root.render(<Foo />);
614 });
615 }).rejects.toThrow("Cannot read properties of undefined (reading 'world')");
616
617 assertConsoleErrorDev([
618 'Foo defines an invalid contextType. ' +
619 'contextType should point to the Context object returned by React.createContext(). ' +
620 'However, it is set to a string.\n' +
621 ' in Foo (at **)',
622 ]);
623 });
624
625 it('should warn if you define contextType on a function component', async () => {
626 const Context = React.createContext();
627
628 function ComponentA() {
629 return <div />;
630 }
631 ComponentA.contextType = Context;
632
633 function ComponentB() {
634 return <div />;
635 }
636 ComponentB.contextType = Context;
637
638 const root = ReactDOMClient.createRoot(document.createElement('div'));
639 await act(() => {
640 root.render(<ComponentA />);
641 });
642 assertConsoleErrorDev([
643 'ComponentA: Function components do not support contextType.\n' +
644 ' in ComponentA (at **)',
645 ]);
646
647 // Warnings should be deduped by component type
648 await act(() => {
649 root.render(<ComponentA />);
650 });
651
652 await act(() => {
653 root.render(<ComponentB />);
654 });
655 assertConsoleErrorDev([
656 'ComponentB: Function components do not support contextType.\n' +
657 ' in ComponentB (at **)',
658 ]);
659 });
660 });