diff --git a/CHANGELOG.md b/CHANGELOG.md index 436817f2a6fc3..34df0e6cde7c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,15 @@ +## 16.8.6 (March 27, 2019) + +### React DOM + +* Fix an incorrect bailout in `useReducer()`. ([@acdlite](https://github.com/acdlite) in [#15124](https://github.com/facebook/react/pull/15124)) +* Fix iframe warnings in Safari DevTools. ([@renanvalentin](https://github.com/renanvalentin) in [#15099](https://github.com/facebook/react/pull/15099)) +* Warn if `contextType` is set to `Context.Consumer` instead of `Context`. ([@aweary](https://github.com/aweary) in [#14831](https://github.com/facebook/react/pull/14831)) +* Warn if `contextType` is set to invalid values. ([@gaearon](https://github.com/gaearon) in [#15142](https://github.com/facebook/react/pull/15142)) + ## 16.8.5 (March 22, 2019) ### React DOM diff --git a/packages/create-subscription/package.json b/packages/create-subscription/package.json index 991c885995366..09e10b88bb15e 100644 --- a/packages/create-subscription/package.json +++ b/packages/create-subscription/package.json @@ -1,7 +1,7 @@ { "name": "create-subscription", "description": "utility for subscribing to external data sources inside React components", - "version": "16.8.5", + "version": "16.8.6", "repository": { "type": "git", "url": "https://github.com/facebook/react.git", diff --git a/packages/jest-react/package.json b/packages/jest-react/package.json index 61b8ffce917ee..c160bcaebfdc7 100644 --- a/packages/jest-react/package.json +++ b/packages/jest-react/package.json @@ -1,6 +1,6 @@ { "name": "jest-react", - "version": "0.6.5", + "version": "0.6.6", "description": "Jest matchers and utilities for testing React components.", "main": "index.js", "repository": { diff --git a/packages/react-art/package.json b/packages/react-art/package.json index e4e1e93bc6a07..2d314c7d30855 100644 --- a/packages/react-art/package.json +++ b/packages/react-art/package.json @@ -1,7 +1,7 @@ { "name": "react-art", "description": "React ART is a JavaScript library for drawing vector graphics using React. It provides declarative and reactive bindings to the ART library. Using the same declarative API you can render the output to either Canvas, SVG or VML (IE8).", - "version": "16.8.5", + "version": "16.8.6", "main": "index.js", "repository": { "type": "git", @@ -27,7 +27,7 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.5" + "scheduler": "^0.13.6" }, "peerDependencies": { "react": "^16.0.0" diff --git a/packages/react-dom/package.json b/packages/react-dom/package.json index 1dd3b9316b5a9..5d1ad1d8901e6 100644 --- a/packages/react-dom/package.json +++ b/packages/react-dom/package.json @@ -1,6 +1,6 @@ { "name": "react-dom", - "version": "16.8.5", + "version": "16.8.6", "description": "React package for working with the DOM.", "main": "index.js", "repository": { @@ -20,7 +20,7 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.5" + "scheduler": "^0.13.6" }, "peerDependencies": { "react": "^16.0.0" diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js index 14e14fccc5e47..8c68b07a5fe88 100644 --- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js +++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js @@ -904,4 +904,125 @@ describe('ReactDOMServer', () => { ' in App (at **)', ]); }); + + it('should warn if an invalid contextType is defined', () => { + const Context = React.createContext(); + + class ComponentA extends React.Component { + // It should warn for both Context.Consumer and Context.Provider + static contextType = Context.Consumer; + render() { + return
; + } + } + class ComponentB extends React.Component { + static contextType = Context.Provider; + render() { + return
; + } + } + + expect(() => { + ReactDOMServer.renderToString(); + }).toWarnDev( + 'Warning: ComponentA defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'Did you accidentally pass the Context.Consumer instead?', + {withoutStack: true}, + ); + + // Warnings should be deduped by component type + ReactDOMServer.renderToString(); + + expect(() => { + ReactDOMServer.renderToString(); + }).toWarnDev( + 'Warning: ComponentB defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'Did you accidentally pass the Context.Provider instead?', + {withoutStack: true}, + ); + }); + + it('should not warn when class contextType is null', () => { + class Foo extends React.Component { + static contextType = null; // Handy for conditional declaration + render() { + return this.context.hello.world; + } + } + + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow("Cannot read property 'world' of undefined"); + }); + + it('should warn when class contextType is undefined', () => { + class Foo extends React.Component { + // This commonly happens with circular deps + // https://github.com/facebook/react/issues/13969 + static contextType = undefined; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow("Cannot read property 'world' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, ' + + 'so try moving the createContext() call to a separate file.', + {withoutStack: true}, + ); + }); + + it('should warn when class contextType is an object', () => { + class Foo extends React.Component { + // Can happen due to a typo + static contextType = { + x: 42, + y: 'hello', + }; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow("Cannot read property 'hello' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to an object with keys {x, y}.', + {withoutStack: true}, + ); + }); + + it('should warn when class contextType is a primitive', () => { + class Foo extends React.Component { + static contextType = 'foo'; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactDOMServer.renderToString(); + }).toThrow("Cannot read property 'world' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to a string.', + {withoutStack: true}, + ); + }); }); diff --git a/packages/react-dom/src/client/ReactInputSelection.js b/packages/react-dom/src/client/ReactInputSelection.js index 54ad4a937f336..f2d0987608b3e 100644 --- a/packages/react-dom/src/client/ReactInputSelection.js +++ b/packages/react-dom/src/client/ReactInputSelection.js @@ -40,15 +40,29 @@ function isInDocument(node) { ); } +function isSameOriginFrame(iframe) { + try { + // Accessing the contentDocument of a HTMLIframeElement can cause the browser + // to throw, e.g. if it has a cross-origin src attribute. + // Safari will show an error in the console when the access results in "Blocked a frame with origin". e.g: + // iframe.contentDocument.defaultView; + // A safety way is to access one of the cross origin properties: Window or Location + // Which might result in "SecurityError" DOM Exception and it is compatible to Safari. + // https://html.spec.whatwg.org/multipage/browsers.html#integration-with-idl + + return typeof iframe.contentWindow.location.href === 'string'; + } catch (err) { + return false; + } +} + function getActiveElementDeep() { let win = window; let element = getActiveElement(); while (element instanceof win.HTMLIFrameElement) { - // Accessing the contentDocument of a HTMLIframeElement can cause the browser - // to throw, e.g. if it has a cross-origin src attribute - try { - win = element.contentDocument.defaultView; - } catch (e) { + if (isSameOriginFrame(element)) { + win = element.contentWindow; + } else { return element; } element = getActiveElement(win.document); diff --git a/packages/react-dom/src/server/ReactPartialRendererContext.js b/packages/react-dom/src/server/ReactPartialRendererContext.js index d7ceaa09758f8..2ad914eb2b1ad 100644 --- a/packages/react-dom/src/server/ReactPartialRendererContext.js +++ b/packages/react-dom/src/server/ReactPartialRendererContext.js @@ -10,19 +10,19 @@ import type {ThreadID} from './ReactThreadIDAllocator'; import type {ReactContext} from 'shared/ReactTypes'; -import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import ReactSharedInternals from 'shared/ReactSharedInternals'; import getComponentName from 'shared/getComponentName'; import warningWithoutStack from 'shared/warningWithoutStack'; import checkPropTypes from 'prop-types/checkPropTypes'; let ReactDebugCurrentFrame; +let didWarnAboutInvalidateContextType; if (__DEV__) { ReactDebugCurrentFrame = ReactSharedInternals.ReactDebugCurrentFrame; + didWarnAboutInvalidateContextType = new Set(); } -const didWarnAboutInvalidateContextType = {}; - export const emptyObject = {}; if (__DEV__) { Object.freeze(emptyObject); @@ -75,22 +75,49 @@ export function processContext( threadID: ThreadID, ) { const contextType = type.contextType; - if (typeof contextType === 'object' && contextType !== null) { - if (__DEV__) { - if (contextType.$$typeof !== REACT_CONTEXT_TYPE) { - let name = getComponentName(type) || 'Component'; - if (!didWarnAboutInvalidateContextType[name]) { - didWarnAboutInvalidateContextType[name] = true; - warningWithoutStack( - false, - '%s defines an invalid contextType. ' + - 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Provider instead?', - name, - ); + if (__DEV__) { + if ('contextType' in (type: any)) { + let isValid = + // Allow null for conditional declaration + contextType === null || + (contextType !== undefined && + contextType.$$typeof === REACT_CONTEXT_TYPE && + contextType._context === undefined); // Not a + + if (!isValid && !didWarnAboutInvalidateContextType.has(type)) { + didWarnAboutInvalidateContextType.add(type); + + let addendum = ''; + if (contextType === undefined) { + addendum = + ' However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, so ' + + 'try moving the createContext() call to a separate file.'; + } else if (typeof contextType !== 'object') { + addendum = ' However, it is set to a ' + typeof contextType + '.'; + } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { + addendum = ' Did you accidentally pass the Context.Provider instead?'; + } else if (contextType._context !== undefined) { + // + addendum = ' Did you accidentally pass the Context.Consumer instead?'; + } else { + addendum = + ' However, it is set to an object with keys {' + + Object.keys(contextType).join(', ') + + '}.'; } + warningWithoutStack( + false, + '%s defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext().%s', + getComponentName(type) || 'Component', + addendum, + ); } } + } + if (typeof contextType === 'object' && contextType !== null) { validateContextBounds(contextType, threadID); return contextType[threadID]; } else { diff --git a/packages/react-is/package.json b/packages/react-is/package.json index fdd9b14ea6cb5..6e502546f89e8 100644 --- a/packages/react-is/package.json +++ b/packages/react-is/package.json @@ -1,6 +1,6 @@ { "name": "react-is", - "version": "16.8.5", + "version": "16.8.6", "description": "Brand checking of React Elements.", "main": "index.js", "repository": { diff --git a/packages/react-reconciler/package.json b/packages/react-reconciler/package.json index 400086ad21979..28d0e7220eddf 100644 --- a/packages/react-reconciler/package.json +++ b/packages/react-reconciler/package.json @@ -1,7 +1,7 @@ { "name": "react-reconciler", "description": "React package for creating custom renderers.", - "version": "0.20.3", + "version": "0.20.4", "keywords": [ "react" ], @@ -33,7 +33,7 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.5" + "scheduler": "^0.13.6" }, "browserify": { "transform": [ diff --git a/packages/react-reconciler/src/ReactFiberClassComponent.js b/packages/react-reconciler/src/ReactFiberClassComponent.js index 3875371714f38..17fd298ef7fce 100644 --- a/packages/react-reconciler/src/ReactFiberClassComponent.js +++ b/packages/react-reconciler/src/ReactFiberClassComponent.js @@ -24,7 +24,7 @@ import shallowEqual from 'shared/shallowEqual'; import getComponentName from 'shared/getComponentName'; import invariant from 'shared/invariant'; import warningWithoutStack from 'shared/warningWithoutStack'; -import {REACT_CONTEXT_TYPE} from 'shared/ReactSymbols'; +import {REACT_CONTEXT_TYPE, REACT_PROVIDER_TYPE} from 'shared/ReactSymbols'; import {startPhaseTimer, stopPhaseTimer} from './ReactDebugFiberPerf'; import {resolveDefaultProps} from './ReactFiberLazyComponent'; @@ -513,23 +513,51 @@ function constructClassInstance( let unmaskedContext = emptyContextObject; let context = null; const contextType = ctor.contextType; - if (typeof contextType === 'object' && contextType !== null) { - if (__DEV__) { - if ( - contextType.$$typeof !== REACT_CONTEXT_TYPE && - !didWarnAboutInvalidateContextType.has(ctor) - ) { + + if (__DEV__) { + if ('contextType' in ctor) { + let isValid = + // Allow null for conditional declaration + contextType === null || + (contextType !== undefined && + contextType.$$typeof === REACT_CONTEXT_TYPE && + contextType._context === undefined); // Not a + + if (!isValid && !didWarnAboutInvalidateContextType.has(ctor)) { didWarnAboutInvalidateContextType.add(ctor); + + let addendum = ''; + if (contextType === undefined) { + addendum = + ' However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, so ' + + 'try moving the createContext() call to a separate file.'; + } else if (typeof contextType !== 'object') { + addendum = ' However, it is set to a ' + typeof contextType + '.'; + } else if (contextType.$$typeof === REACT_PROVIDER_TYPE) { + addendum = ' Did you accidentally pass the Context.Provider instead?'; + } else if (contextType._context !== undefined) { + // + addendum = ' Did you accidentally pass the Context.Consumer instead?'; + } else { + addendum = + ' However, it is set to an object with keys {' + + Object.keys(contextType).join(', ') + + '}.'; + } warningWithoutStack( false, '%s defines an invalid contextType. ' + - 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Provider instead?', + 'contextType should point to the Context object returned by React.createContext().%s', getComponentName(ctor) || 'Component', + addendum, ); } } + } + if (typeof contextType === 'object' && contextType !== null) { context = readContext((contextType: any)); } else { unmaskedContext = getUnmaskedContext(workInProgress, ctor, true); diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js index baa07a4c59b52..ca33a6dff57c7 100644 --- a/packages/react-reconciler/src/ReactFiberHooks.js +++ b/packages/react-reconciler/src/ReactFiberHooks.js @@ -89,8 +89,8 @@ type Update = { type UpdateQueue = { last: Update | null, dispatch: (A => mixed) | null, - eagerReducer: ((S, A) => S) | null, - eagerState: S | null, + lastRenderedReducer: ((S, A) => S) | null, + lastRenderedState: S | null, }; export type HookType = @@ -591,8 +591,8 @@ function mountReducer( const queue = (hook.queue = { last: null, dispatch: null, - eagerReducer: reducer, - eagerState: (initialState: any), + lastRenderedReducer: reducer, + lastRenderedState: (initialState: any), }); const dispatch: Dispatch = (queue.dispatch = (dispatchAction.bind( null, @@ -615,6 +615,8 @@ function updateReducer( 'Should have a queue. This is likely a bug in React. Please file an issue.', ); + queue.lastRenderedReducer = reducer; + if (numberOfReRenders > 0) { // This is a re-render. Apply the new render phase updates to the previous // work-in-progress hook. @@ -650,8 +652,7 @@ function updateReducer( hook.baseState = newState; } - queue.eagerReducer = reducer; - queue.eagerState = newState; + queue.lastRenderedState = newState; return [newState, dispatch]; } @@ -730,8 +731,7 @@ function updateReducer( hook.baseUpdate = newBaseUpdate; hook.baseState = newBaseState; - queue.eagerReducer = reducer; - queue.eagerState = newState; + queue.lastRenderedState = newState; } const dispatch: Dispatch = (queue.dispatch: any); @@ -749,8 +749,8 @@ function mountState( const queue = (hook.queue = { last: null, dispatch: null, - eagerReducer: basicStateReducer, - eagerState: (initialState: any), + lastRenderedReducer: basicStateReducer, + lastRenderedState: (initialState: any), }); const dispatch: Dispatch< BasicStateAction, @@ -1129,21 +1129,21 @@ function dispatchAction( // The queue is currently empty, which means we can eagerly compute the // next state before entering the render phase. If the new state is the // same as the current state, we may be able to bail out entirely. - const eagerReducer = queue.eagerReducer; - if (eagerReducer !== null) { + const lastRenderedReducer = queue.lastRenderedReducer; + if (lastRenderedReducer !== null) { let prevDispatcher; if (__DEV__) { prevDispatcher = ReactCurrentDispatcher.current; ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV; } try { - const currentState: S = (queue.eagerState: any); - const eagerState = eagerReducer(currentState, action); + const currentState: S = (queue.lastRenderedState: any); + const eagerState = lastRenderedReducer(currentState, action); // Stash the eagerly computed state, and the reducer used to compute // it, on the update object. If the reducer hasn't changed by the // time we enter the render phase, then the eager state can be used // without calling the reducer again. - update.eagerReducer = eagerReducer; + update.eagerReducer = lastRenderedReducer; update.eagerState = eagerState; if (is(eagerState, currentState)) { // Fast path. We can bail out without scheduling React to re-render. diff --git a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js index 192c077c1c79f..911eda978e4e3 100644 --- a/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js +++ b/packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.internal.js @@ -1871,4 +1871,89 @@ describe('ReactHooksWithNoopRenderer', () => { // ); }); }); + + it('eager bailout optimization should always compare to latest rendered reducer', () => { + // Edge case based on a bug report + let setCounter; + function App() { + const [counter, _setCounter] = useState(1); + setCounter = _setCounter; + return ; + } + + function Component({count}) { + const [state, dispatch] = useReducer(() => { + // This reducer closes over a value from props. If the reducer is not + // properly updated, the eager reducer will compare to an old value + // and bail out incorrectly. + ReactNoop.yield('Reducer: ' + count); + return count; + }, -1); + useEffect( + () => { + ReactNoop.yield('Effect: ' + count); + dispatch(); + }, + [count], + ); + ReactNoop.yield('Render: ' + state); + return ; + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual(['Render: -1']); + ReactNoop.flushPassiveEffects(); + expect(ReactNoop.flush()).toEqual([ + 'Effect: 1', + 'Reducer: 1', + 'Reducer: 1', + 'Render: 1', + ]); + expect(ReactNoop.getChildren()).toEqual([span(1)]); + + act(() => { + setCounter(2); + }); + expect(ReactNoop.flush()).toEqual([ + 'Render: 1', + 'Effect: 2', + 'Reducer: 2', + 'Reducer: 2', + 'Render: 2', + ]); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + }); + + it('should update latest rendered reducer when a preceding state receives a render phase update', () => { + // Similar to previous test, except using a preceding render phase update + // instead of new props. + let dispatch; + function App() { + const [step, setStep] = useState(0); + const [shadow, _dispatch] = useReducer(() => step, step); + dispatch = _dispatch; + + if (step < 5) { + setStep(step + 1); + } + + ReactNoop.yield(`Step: ${step}, Shadow: ${shadow}`); + return ; + } + + ReactNoop.render(); + expect(ReactNoop.flush()).toEqual([ + 'Step: 0, Shadow: 0', + 'Step: 1, Shadow: 0', + 'Step: 2, Shadow: 0', + 'Step: 3, Shadow: 0', + 'Step: 4, Shadow: 0', + 'Step: 5, Shadow: 0', + ]); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + + act(() => dispatch()); + expect(ReactNoop.flush()).toEqual(['Step: 5, Shadow: 5']); + expect(ReactNoop.getChildren()).toEqual([span(5)]); + }); }); diff --git a/packages/react-test-renderer/package.json b/packages/react-test-renderer/package.json index fec8e7f7a79b9..3aba1daa4ae2b 100644 --- a/packages/react-test-renderer/package.json +++ b/packages/react-test-renderer/package.json @@ -1,6 +1,6 @@ { "name": "react-test-renderer", - "version": "16.8.5", + "version": "16.8.6", "description": "React package for snapshot testing.", "main": "index.js", "repository": { @@ -21,8 +21,8 @@ "dependencies": { "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "react-is": "^16.8.5", - "scheduler": "^0.13.5" + "react-is": "^16.8.6", + "scheduler": "^0.13.6" }, "peerDependencies": { "react": "^16.0.0" diff --git a/packages/react/package.json b/packages/react/package.json index 1ccdae0ace355..fb0216747f2b1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -4,7 +4,7 @@ "keywords": [ "react" ], - "version": "16.8.5", + "version": "16.8.6", "homepage": "https://reactjs.org/", "bugs": "https://github.com/facebook/react/issues", "license": "MIT", @@ -29,7 +29,7 @@ "loose-envify": "^1.1.0", "object-assign": "^4.1.1", "prop-types": "^15.6.2", - "scheduler": "^0.13.5" + "scheduler": "^0.13.6" }, "browserify": { "transform": [ diff --git a/packages/react/src/__tests__/ReactContextValidator-test.js b/packages/react/src/__tests__/ReactContextValidator-test.js index 7ee787e2250d4..bc935753b6e55 100644 --- a/packages/react/src/__tests__/ReactContextValidator-test.js +++ b/packages/react/src/__tests__/ReactContextValidator-test.js @@ -541,9 +541,10 @@ describe('ReactContextValidator', () => { it('should warn if an invalid contextType is defined', () => { const Context = React.createContext(); - + // This tests that both Context.Consumer and Context.Provider + // warn about invalid contextType. class ComponentA extends React.Component { - static contextType = Context.Provider; + static contextType = Context.Consumer; render() { return
; } @@ -560,7 +561,7 @@ describe('ReactContextValidator', () => { }).toWarnDev( 'Warning: ComponentA defines an invalid contextType. ' + 'contextType should point to the Context object returned by React.createContext(). ' + - 'Did you accidentally pass the Context.Provider instead?', + 'Did you accidentally pass the Context.Consumer instead?', {withoutStack: true}, ); @@ -577,6 +578,87 @@ describe('ReactContextValidator', () => { ); }); + it('should not warn when class contextType is null', () => { + class Foo extends React.Component { + static contextType = null; // Handy for conditional declaration + render() { + return this.context.hello.world; + } + } + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow("Cannot read property 'world' of undefined"); + }); + + it('should warn when class contextType is undefined', () => { + class Foo extends React.Component { + // This commonly happens with circular deps + // https://github.com/facebook/react/issues/13969 + static contextType = undefined; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow("Cannot read property 'world' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to undefined. ' + + 'This can be caused by a typo or by mixing up named and default imports. ' + + 'This can also happen due to a circular dependency, ' + + 'so try moving the createContext() call to a separate file.', + {withoutStack: true}, + ); + }); + + it('should warn when class contextType is an object', () => { + class Foo extends React.Component { + // Can happen due to a typo + static contextType = { + x: 42, + y: 'hello', + }; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow("Cannot read property 'hello' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to an object with keys {x, y}.', + {withoutStack: true}, + ); + }); + + it('should warn when class contextType is a primitive', () => { + class Foo extends React.Component { + static contextType = 'foo'; + render() { + return this.context.hello.world; + } + } + + expect(() => { + expect(() => { + ReactTestUtils.renderIntoDocument(); + }).toThrow("Cannot read property 'world' of undefined"); + }).toWarnDev( + 'Foo defines an invalid contextType. ' + + 'contextType should point to the Context object returned by React.createContext(). ' + + 'However, it is set to a string.', + {withoutStack: true}, + ); + }); + it('should warn if you define contextType on a function component', () => { const Context = React.createContext(); diff --git a/packages/scheduler/package.json b/packages/scheduler/package.json index 3845459764d71..9acdaae6903c7 100644 --- a/packages/scheduler/package.json +++ b/packages/scheduler/package.json @@ -1,6 +1,6 @@ { "name": "scheduler", - "version": "0.13.5", + "version": "0.13.6", "description": "Cooperative scheduler for the browser environment.", "main": "index.js", "repository": { diff --git a/packages/shared/ReactVersion.js b/packages/shared/ReactVersion.js index 0679d87d34f44..2496b912cac1c 100644 --- a/packages/shared/ReactVersion.js +++ b/packages/shared/ReactVersion.js @@ -8,4 +8,4 @@ 'use strict'; // TODO: this is special because it gets imported during build. -module.exports = '16.8.5'; +module.exports = '16.8.6';