diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 254abf87b27a..034f765d97a4 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -12,9 +12,6 @@ src/isomorphic/classic/__tests__/ReactContextValidator-test.js src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js * includes the owner name when passing null, undefined, boolean, or number -src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js -* should give context for PropType errors in nested components. - src/renderers/dom/__tests__/ReactDOMProduction-test.js * should throw with an error code in production @@ -23,14 +20,11 @@ src/renderers/dom/shared/__tests__/ReactDOM-test.js * throws in render() if the update callback is not a function src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js -* should warn for children on void elements -* should report component containing invalid styles * should clean up input value tracking * should clean up input textarea tracking -* gives source code refs for unknown prop warning -* gives source code refs for unknown prop warning for update render -* gives source code refs for unknown prop warning for exact elements -* gives source code refs for unknown prop warning for exact elements in composition +* gives source code refs for unknown prop warning (ssr) +* gives source code refs for unknown prop warning for exact elements (ssr) +* gives source code refs for unknown prop warning for exact elements in composition (ssr) src/renderers/dom/shared/__tests__/ReactDOMTextComponent-test.js * can reconcile text merged by Node.normalize() alongside other elements @@ -64,7 +58,6 @@ src/renderers/shared/__tests__/ReactPerf-test.js * should work when measurement starts during reconciliation src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js -* gets created * can be retrieved by ID src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index 0b604fcf6f56..6464c7d7ee54 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -1,33 +1,7 @@ src/addons/transitions/__tests__/ReactTransitionGroup-test.js * should warn for duplicated keys with component stack info -src/isomorphic/classic/__tests__/ReactContextValidator-test.js -* should check context types -* should check child context types - -src/isomorphic/classic/element/__tests__/ReactElementClone-test.js -* should check declared prop types after clone - -src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js -* warns for keys for arrays of elements with owner info -* warns for keys with component stack info -* should give context for PropType errors in nested components. - -src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js -* warns for keys for arrays of elements with owner info - -src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js -* should warn when using hyphenated style names -* should warn when updating hyphenated style names -* warns when miscapitalizing vendored style names -* should warn about style having a trailing semicolon -* should warn about style containing a NaN value - src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js -* should warn for unknown prop -* should group multiple unknown prop warnings together -* should warn for onDblClick prop -* should emit a warning once for a named custom component using shady DOM * should not warn when server-side rendering `onScroll` * warns on invalid nesting * warns on invalid nesting at root @@ -35,15 +9,8 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * gives useful context in warnings * should warn about incorrect casing on properties (ssr) * should warn about incorrect casing on event handlers (ssr) -* should warn about incorrect casing on properties -* should warn about incorrect casing on event handlers -* should warn about class -* should suggest property name if available - -src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js -* should warn for one invalid aria-* prop -* should warn for many invalid aria-* props -* should warn for an improperly cased aria-* prop +* should warn about class (ssr) +* should suggest property name if available (ssr) src/renderers/dom/shared/__tests__/ReactMount-test.js * should warn if mounting into dirty rendered markup @@ -59,17 +26,6 @@ src/renderers/dom/shared/__tests__/ReactMountDestruction-test.js src/renderers/dom/shared/__tests__/ReactServerRendering-test.js * should have the correct mounting behavior -src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js -* should warn if value is null -* should warn if controlled input switches to uncontrolled (value is null) -* should warn if uncontrolled input (value is null) switches to controlled - -src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js -* should warn if value is null - -src/renderers/dom/shared/wrappers/__tests__/ReactDOMTextarea-test.js -* should warn if value is null - src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js * uses displayName or Unknown for classic components * uses displayName, name, or ReactComponent for modern components @@ -136,12 +92,8 @@ src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js * gets recorded during an update * gets ignored if the styles are shallowly equal * gets recorded during mount -* gets recorded during an update -* gets recorded as a removal during an update * gets recorded during mount -* gets recorded during an update * gets recorded during mount -* gets recorded during an update * gets recorded during an update from text content * gets recorded during an update from html * gets recorded during an update from children @@ -154,10 +106,6 @@ src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js * gets reported when a child is inserted * gets reported when a child is removed -src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js -* warns for duplicated keys -* warns for duplicated keys with component stack info - src/renderers/shared/shared/__tests__/ReactComponentLifeCycle-test.js * should correctly determine if a component is mounted * should correctly determine if a null component is mounted @@ -167,17 +115,10 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js * should warn about `forceUpdate` on unmounted components * should warn about `setState` on unmounted components * should disallow nested render calls -* should warn when mutated props are passed src/renderers/shared/shared/__tests__/ReactMultiChild-test.js -* should warn for duplicated keys with component stack info * should warn for using maps as children with owner info src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js * should warn for childContextTypes on a functional component * should warn when given a ref -* should use correct name in key warning - -src/renderers/shared/shared/__tests__/refs-test.js -* attaches, detaches from fiber component with stack layer -* attaches, detaches from stack component with fiber layer diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 2c127f082de0..e4e9a496b724 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -153,6 +153,8 @@ src/isomorphic/children/__tests__/sliceChildren-test.js src/isomorphic/classic/__tests__/ReactContextValidator-test.js * should filter out context not in contextTypes * should pass next context to lifecycles +* should check context types +* should check child context types src/isomorphic/classic/class/__tests__/ReactBind-test.js * Holds reference to instance @@ -266,20 +268,24 @@ src/isomorphic/classic/element/__tests__/ReactElementClone-test.js * does not warns for arrays of elements with keys * does not warn when the element is directly in rest args * does not warn when the array contains a non-element +* should check declared prop types after clone * should ignore key and ref warning getters * should ignore undefined key and ref * should extract null key and ref src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js * warns for keys for arrays of elements in rest args +* warns for keys for arrays of elements with owner info * warns for keys for arrays with no owner or parent info * warns for keys for arrays of elements with no owner info +* warns for keys with component stack info * does not warn for keys when passing children down * warns for keys for iterables of elements in rest args * does not warns for arrays of elements with keys * does not warns for iterable elements with keys * does not warn when the element is directly in rest args * does not warn when the array contains a non-element +* should give context for PropType errors in nested components. * gives a helpful error when passing null, undefined, boolean, or number * should check default prop values * should not check the default for explicit null @@ -471,12 +477,15 @@ src/isomorphic/modern/element/__tests__/ReactJSXElement-test.js src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js * warns for keys for arrays of elements in children position +* warns for keys for arrays of elements with owner info * warns for keys for iterables of elements in rest args * does not warns for arrays of elements with keys * does not warns for iterable elements with keys * does not warn for numeric keys in entry iterable as a child * does not warn when the element is directly as children * does not warn when the child array contains non-elements +* should give context for PropType errors in nested components. +* should update component stack after receiving next element * gives a helpful error when passing null, undefined, or boolean * should check default prop values * should not check the default for explicit null @@ -543,6 +552,11 @@ src/renderers/dom/shared/__tests__/CSSPropertyOperations-test.js * should create vendor-prefixed markup correctly * should set style attribute when styles exist * should not set style attribute when no styles exist +* should warn when using hyphenated style names +* should warn when updating hyphenated style names +* warns when miscapitalizing vendored style names +* should warn about style having a trailing semicolon +* should warn about style containing a NaN value src/renderers/dom/shared/__tests__/DOMPropertyOperations-test.js * should create markup for simple properties @@ -607,6 +621,9 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should gracefully handle various style value types * should not update styles when mutating a proxy style object * should throw when mutating style objectsd +* should warn for unknown prop +* should group multiple unknown prop warnings together +* should warn for onDblClick prop * should not warn for "0" as a unitless style value * should warn nicely about NaN in style * should update styles if initially null @@ -644,6 +661,8 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should warn on upper case HTML tags, not SVG nor custom tags * should warn against children for void elements * should warn against dangerouslySetInnerHTML for void elements +* should include owner rather than parent in warnings +* should emit a warning once for a named custom component using shady DOM * should emit a warning once for an unnamed custom component using shady DOM * should treat menuitem as a void element but still create the closing tag * should validate against multiple children props @@ -656,12 +675,14 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should validate against invalid styles * should track input values * should track textarea values +* should warn for children on void elements * should support custom elements which extend native elements * should warn against children for void elements * should warn against dangerouslySetInnerHTML for void elements * should validate against multiple children props * should warn about contentEditable and children * should validate against invalid styles +* should report component containing invalid styles * should properly escape text content and attributes values * unmounts children before unsetting DOM node info * should warn about the `onScroll` issue when unsupported (IE8) @@ -669,7 +690,16 @@ src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js * should throw when an attack vector is used server-side * should throw when an invalid tag name is used * should throw when an attack vector is used +* should warn about incorrect casing on properties +* should warn about incorrect casing on event handlers +* should warn about class * should warn about props that are no longer supported +* should warn about props that are no longer supported (ssr) +* gives source code refs for unknown prop warning +* gives source code refs for unknown prop warning for update render +* gives source code refs for unknown prop warning for exact elements +* gives source code refs for unknown prop warning for exact elements in composition +* should suggest property name if available src/renderers/dom/shared/__tests__/ReactDOMComponentTree-test.js * finds nodes for instances @@ -680,6 +710,9 @@ src/renderers/dom/shared/__tests__/ReactDOMIDOperations-test.js src/renderers/dom/shared/__tests__/ReactDOMInvalidARIAHook-test.js * should allow valid aria-* props +* should warn for one invalid aria-* prop +* should warn for many invalid aria-* props +* should warn for an improperly cased aria-* prop src/renderers/dom/shared/__tests__/ReactDOMSVG-test.js * creates initial namespaced markup @@ -933,11 +966,14 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMInput-test.js * should have a this value of undefined if bind is not used * should warn with checked and no onChange handler with readOnly specified * should update defaultValue to empty string +* should warn if value is null * should warn if checked and defaultChecked props are specified * should warn if value and defaultValue props are specified * should warn if controlled input switches to uncontrolled (value is undefined) +* should warn if controlled input switches to uncontrolled (value is null) * should warn if controlled input switches to uncontrolled with defaultValue * should warn if uncontrolled input (value is undefined) switches to controlled +* should warn if uncontrolled input (value is null) switches to controlled * should warn if controlled checkbox switches to uncontrolled (checked is undefined) * should warn if controlled checkbox switches to uncontrolled (checked is null) * should warn if controlled checkbox switches to uncontrolled with defaultChecked @@ -983,6 +1019,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMSelect-test.js * should support server-side rendering with defaultValue * should support server-side rendering with multiple * should not control defaultValue if readding options +* should warn if value is null * should refresh state on change * should warn if value and defaultValue props are specified * should be able to safely remove select onChange @@ -1015,6 +1052,7 @@ src/renderers/dom/shared/wrappers/__tests__/ReactDOMTextarea-test.js * should allow objects as children * should throw with multiple or invalid children * should unmount +* should warn if value is null * should warn if value and defaultValue are specified src/renderers/native/__tests__/ReactNativeAttributePayload-test.js @@ -1204,6 +1242,7 @@ src/renderers/shared/fiber/__tests__/ReactTopLevelText-test.js * should render a component returning numbers directly from render src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.js +* gets created * is created during mounting * is created when calling renderToString during render @@ -1258,11 +1297,21 @@ src/renderers/shared/hooks/__tests__/ReactComponentTreeHook-test.native.js src/renderers/shared/hooks/__tests__/ReactHostOperationHistoryHook-test.js * gets ignored for composite roots that return null +* gets recorded during an update +* gets recorded as a removal during an update +* gets recorded during an update +* gets recorded during an update * gets ignored if new text is equal * gets ignored if new text is equal * gets ignored if the type has not changed * gets ignored if new html is equal +src/renderers/shared/shared/__tests__/ReactChildReconciler-test.js +* warns for duplicated array keys +* warns for duplicated array keys with component stack info +* warns for duplicated iterable keys +* warns for duplicated iterable keys with component stack info + src/renderers/shared/shared/__tests__/ReactComponent-test.js * should throw when supplying a ref outside of render method * should warn when children are mutated during render @@ -1317,6 +1366,7 @@ src/renderers/shared/shared/__tests__/ReactCompositeComponent-test.js * should replace state * should support objects with prototypes as state * should not warn about unmounting during unmounting +* should warn when mutated props are passed * should only call componentWillUnmount once src/renderers/shared/shared/__tests__/ReactCompositeComponentDOMMinimalism-test.js @@ -1404,6 +1454,8 @@ src/renderers/shared/shared/__tests__/ReactMultiChild-test.js * should replace children with different constructors * should NOT replace children with different owners * should replace children with different keys +* should warn for duplicated array keys with component stack info +* should warn for duplicated iterable keys with component stack info * should reorder bailed-out children src/renderers/shared/shared/__tests__/ReactMultiChildReconcile-test.js @@ -1455,6 +1507,7 @@ src/renderers/shared/shared/__tests__/ReactStatelessComponent-test.js * should unmount stateless component * should pass context thru stateless component * should provide a null ref +* should use correct name in key warning * should support default props and prop types * should receive context * should work with arrow functions @@ -1509,6 +1562,8 @@ src/renderers/shared/shared/__tests__/refs-test.js * ref called correctly for stateless component when __DEV__ = false * ref called correctly for stateless component when __DEV__ = true * coerces numbers to strings +* attaches, detaches from fiber component with stack layer +* attaches, detaches from stack component with fiber layer src/renderers/shared/shared/event/__tests__/EventPluginHub-test.js * should prevent non-function listeners, at dispatch diff --git a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js index 11dfb266b9bc..6783f762697f 100644 --- a/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js +++ b/src/isomorphic/classic/element/__tests__/ReactElementValidator-test.js @@ -258,9 +258,6 @@ describe('ReactElementValidator', () => { expectDev(console.error.calls.count()).toBe(0); }); - // TODO: These warnings currently come from the composite component, but - // they should be moved into the ReactElementValidator. - it('should give context for PropType errors in nested components.', () => { // In this test, we're making sure that if a proptype error is found in a // component, we give a small hint as to which parent instantiated that diff --git a/src/isomorphic/hooks/ReactComponentTreeHook.js b/src/isomorphic/hooks/ReactComponentTreeHook.js index a5fdd85a58cb..b7d253df86ea 100644 --- a/src/isomorphic/hooks/ReactComponentTreeHook.js +++ b/src/isomorphic/hooks/ReactComponentTreeHook.js @@ -13,6 +13,13 @@ 'use strict'; var ReactCurrentOwner = require('ReactCurrentOwner'); +var ReactTypeOfWork = require('ReactTypeOfWork'); +var { + IndeterminateComponent, + FunctionalComponent, + ClassComponent, + HostComponent, +} = ReactTypeOfWork; var getComponentName = require('getComponentName'); var invariant = require('invariant'); @@ -20,6 +27,7 @@ var warning = require('warning'); import type { ReactElement, Source } from 'ReactElementType'; import type { DebugID } from 'ReactInstanceType'; +import type { Fiber } from 'ReactFiber'; function isNative(fn) { // Based on isNative() from Lodash @@ -191,6 +199,25 @@ function describeID(id: DebugID): string { return describeComponentFrame(name, element && element._source, ownerName); } +function describeFiber(fiber : Fiber) : string { + switch (fiber.tag) { + case IndeterminateComponent: + case FunctionalComponent: + case ClassComponent: + case HostComponent: + var owner = fiber._debugOwner; + var source = fiber._debugSource; + var name = getComponentName(fiber); + var ownerName = null; + if (owner) { + ownerName = getComponentName(owner); + } + return describeComponentFrame(name, source, ownerName); + default: + return ''; + } +} + var ReactComponentTreeHook = { onSetChildren(id: DebugID, nextChildIDs: Array): void { var item = getItem(id); @@ -324,9 +351,15 @@ var ReactComponentTreeHook = { } var currentOwner = ReactCurrentOwner.current; - if (currentOwner && typeof currentOwner._debugID === 'number') { - var id = currentOwner && currentOwner._debugID; - info += ReactComponentTreeHook.getStackAddendumByID(id); + if (currentOwner) { + if (typeof currentOwner.tag === 'number') { + const workInProgress = ((currentOwner : any) : Fiber); + // Safe because if current owner exists, we are reconciling, + // and it is guaranteed to be the work-in-progress version. + info += ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress); + } else if (typeof currentOwner._debugID === 'number') { + info += ReactComponentTreeHook.getStackAddendumByID(currentOwner._debugID); + } } return info; }, @@ -340,6 +373,20 @@ var ReactComponentTreeHook = { return info; }, + // This function can only be called with a work-in-progress fiber and + // only during begin or complete phase. Do not call it under any other + // circumstances. + getStackAddendumByWorkInProgressFiber(workInProgress : Fiber) : string { + var info = ''; + var node = workInProgress; + do { + info += describeFiber(node); + // Otherwise this return pointer might point to the wrong tree: + node = node.return; + } while (node); + return info; + }, + getChildIDs(id: DebugID): Array { var item = getItem(id); return item ? item.childIDs : []; diff --git a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js index 47563ff67b21..21d9999ebb55 100644 --- a/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js +++ b/src/isomorphic/modern/element/__tests__/ReactJSXElementValidator-test.js @@ -15,6 +15,7 @@ // of dynamic errors when using JSX with Flow. var React; +var ReactDOM; var ReactTestUtils; describe('ReactJSXElementValidator', () => { @@ -25,6 +26,7 @@ describe('ReactJSXElementValidator', () => { jest.resetModuleRegistry(); React = require('React'); + ReactDOM = require('ReactDOM'); ReactTestUtils = require('ReactTestUtils'); Component = class extends React.Component { @@ -173,9 +175,6 @@ describe('ReactJSXElementValidator', () => { expectDev(console.error.calls.count()).toBe(0); }); - // TODO: These warnings currently come from the composite component, but - // they should be moved into the ReactElementValidator. - it('should give context for PropType errors in nested components.', () => { // In this test, we're making sure that if a proptype error is found in a // component, we give a small hint as to which parent instantiated that @@ -206,6 +205,45 @@ describe('ReactJSXElementValidator', () => { ); }); + it('should update component stack after receiving next element', () => { + spyOn(console, 'error'); + function MyComp() { + return null; + } + MyComp.propTypes = { + color: React.PropTypes.string, + }; + function MiddleComp(props) { + return ; + } + function ParentComp(props) { + if (props.warn) { + // This element has a source thanks to JSX. + return ; + } + // This element has no source. + return React.createElement(MiddleComp, {color: 'blue'}); + } + + var container = document.createElement('div'); + ReactDOM.render(, container); + ReactDOM.render(, container); + + expect(console.error.calls.count()).toBe(1); + // The warning should have the full stack with line numbers. + // If it doesn't, it means we're using information from the old element. + expect( + console.error.calls.argsFor(0)[0].replace(/\(at .+?:\d+\)/g, '(at **)') + ).toBe( + 'Warning: Failed prop type: ' + + 'Invalid prop `color` of type `number` supplied to `MyComp`, ' + + 'expected `string`.\n' + + ' in MyComp (at **)\n' + + ' in MiddleComp (at **)\n' + + ' in ParentComp (at **)' + ); + }); + it('gives a helpful error when passing null, undefined, or boolean', () => { var Undefined = undefined; var Null = null; diff --git a/src/renderers/dom/fiber/ReactDOMFiberComponent.js b/src/renderers/dom/fiber/ReactDOMFiberComponent.js index 44afff611e87..497a3f648d50 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberComponent.js +++ b/src/renderers/dom/fiber/ReactDOMFiberComponent.js @@ -20,21 +20,30 @@ var DOMProperty = require('DOMProperty'); var DOMPropertyOperations = require('DOMPropertyOperations'); var EventPluginRegistry = require('EventPluginRegistry'); var ReactBrowserEventEmitter = require('ReactBrowserEventEmitter'); -var ReactDOMComponentTree = require('ReactDOMComponentTree'); var ReactDOMFiberInput = require('ReactDOMFiberInput'); var ReactDOMFiberOption = require('ReactDOMFiberOption'); var ReactDOMFiberSelect = require('ReactDOMFiberSelect'); var ReactDOMFiberTextarea = require('ReactDOMFiberTextarea'); +var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber'); var emptyFunction = require('emptyFunction'); var focusNode = require('focusNode'); -var getCurrentOwnerName = require('getCurrentOwnerName'); var invariant = require('invariant'); var isEventSupported = require('isEventSupported'); var setInnerHTML = require('setInnerHTML'); var setTextContent = require('setTextContent'); var inputValueTracking = require('inputValueTracking'); var warning = require('warning'); + +if (__DEV__) { + var ReactDOMInvalidARIAHook = require('ReactDOMInvalidARIAHook'); + var ReactDOMNullInputValuePropHook = require('ReactDOMNullInputValuePropHook'); + var ReactDOMUnknownPropertyHook = require('ReactDOMUnknownPropertyHook'); + var { validateProperties: validateARIAProperties } = ReactDOMInvalidARIAHook; + var { validateProperties: validateInputPropertes } = ReactDOMNullInputValuePropHook; + var { validateProperties: validateUnknownPropertes } = ReactDOMUnknownPropertyHook; +} + var didWarnShadyDOM = false; var listenTo = ReactBrowserEventEmitter.listenTo; @@ -56,8 +65,9 @@ var DOC_FRAGMENT_TYPE = 11; function getDeclarationErrorAddendum() { - var ownerName = getCurrentOwnerName(); + var ownerName = getCurrentFiberOwnerName(); if (ownerName) { + // TODO: also report the stack. return ' This DOM node was rendered by `' + ownerName + '`.'; } return ''; @@ -122,6 +132,14 @@ function assertValidProps(tag : string, props : ?Object) { ); } +if (__DEV__) { + var validatePropertiesInDevelopment = function(type, props) { + validateARIAProperties(type, props); + validateInputPropertes(type, props); + validateUnknownPropertes(type, props); + }; +} + function ensureListeningTo(rootContainerElement, registrationName) { if (__DEV__) { // IE8 has no API for event capturing and the `onScroll` event doesn't @@ -424,19 +442,10 @@ function updateDOMProperties( } } if (styleUpdates) { - var componentPlaceholder = null; - if (__DEV__) { - // HACK - var internalInstance = ReactDOMComponentTree.getInstanceFromNode(domElement); - componentPlaceholder = { - _currentElement: { type: internalInstance.type, props: internalInstance.memoizedProps }, - _debugID: internalInstance._debugID, - }; - } + // TODO: call ReactInstrumentation.debugTool.onHostOperation in DEV. CSSPropertyOperations.setValueForStyles( domElement, styleUpdates, - componentPlaceholder // TODO: Change CSSPropertyOperations to use getCurrentOwnerName. ); } } @@ -524,12 +533,13 @@ var ReactDOMFiberComponent = { var isCustomComponentTag = isCustomComponent(tag, rawProps); if (__DEV__) { + validatePropertiesInDevelopment(tag, rawProps); if (isCustomComponentTag && !didWarnShadyDOM && domElement.shadyRoot) { warning( false, '%s is using shady DOM. Using shady DOM with React can ' + 'cause things to break subtly.', - getCurrentOwnerName() || 'A component' + getCurrentFiberOwnerName() || 'A component' ); didWarnShadyDOM = true; } @@ -641,6 +651,10 @@ var ReactDOMFiberComponent = { nextRawProps : Object, rootContainerElement : Element ) : void { + if (__DEV__) { + validatePropertiesInDevelopment(tag, nextRawProps); + } + var lastProps : Object; var nextProps : Object; switch (tag) { diff --git a/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js b/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js index 8b5b4121ac6b..0c34e13744a0 100644 --- a/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js +++ b/src/renderers/dom/fiber/wrappers/ReactDOMFiberInput.js @@ -23,8 +23,8 @@ type InputWithWrapperState = HTMLInputElement & { var DOMPropertyOperations = require('DOMPropertyOperations'); var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes'); var ReactDOMComponentTree = require('ReactDOMComponentTree'); +var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber'); -var getCurrentOwnerName = require('getCurrentOwnerName'); var invariant = require('invariant'); var warning = require('warning'); @@ -86,7 +86,7 @@ var ReactDOMInput = { ReactControlledValuePropTypes.checkPropTypes( 'input', props, - getCurrentOwnerName() + getCurrentFiberOwnerName() ); if ( @@ -102,7 +102,7 @@ var ReactDOMInput = { 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', - getCurrentOwnerName() || 'A component', + getCurrentFiberOwnerName() || 'A component', props.type ); didWarnCheckedDefaultChecked = true; @@ -120,7 +120,7 @@ var ReactDOMInput = { 'both). Decide between using a controlled or uncontrolled input ' + 'element and remove one of these props. More info: ' + 'https://fb.me/react-controlled-components', - getCurrentOwnerName() || 'A component', + getCurrentFiberOwnerName() || 'A component', props.type ); didWarnValueDefaultValue = true; @@ -151,7 +151,7 @@ var ReactDOMInput = { 'Input elements should not switch from uncontrolled to controlled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', - getCurrentOwnerName() || 'A component', + getCurrentFiberOwnerName() || 'A component', props.type ); didWarnUncontrolledToControlled = true; @@ -163,7 +163,7 @@ var ReactDOMInput = { 'Input elements should not switch from controlled to uncontrolled (or vice versa). ' + 'Decide between using a controlled or uncontrolled input ' + 'element for the lifetime of the component. More info: https://fb.me/react-controlled-components', - getCurrentOwnerName() || 'A component', + getCurrentFiberOwnerName() || 'A component', props.type ); didWarnControlledToUncontrolled = true; diff --git a/src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js b/src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js index 9631fddcd920..195a0e610592 100644 --- a/src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js +++ b/src/renderers/dom/fiber/wrappers/ReactDOMFiberSelect.js @@ -20,14 +20,13 @@ type SelectWithWrapperState = HTMLSelectElement & { }; var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes'); - -var getCurrentOwnerName = require('getCurrentOwnerName'); +var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber'); var warning = require('warning'); var didWarnValueDefaultValue = false; function getDeclarationErrorAddendum() { - var ownerName = getCurrentOwnerName(); + var ownerName = getCurrentFiberOwnerName(); if (ownerName) { return ' Check the render method of `' + ownerName + '`.'; } @@ -43,7 +42,7 @@ function checkSelectPropTypes(props) { ReactControlledValuePropTypes.checkPropTypes( 'select', props, - getCurrentOwnerName() + getCurrentFiberOwnerName() ); for (var i = 0; i < valuePropNames.length; i++) { diff --git a/src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js b/src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js index 34ec317fd106..49433ff5a17a 100644 --- a/src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js +++ b/src/renderers/dom/fiber/wrappers/ReactDOMFiberTextarea.js @@ -19,8 +19,8 @@ type TextAreaWithWrapperState = HTMLTextAreaElement & { }; var ReactControlledValuePropTypes = require('ReactControlledValuePropTypes'); +var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber'); -var getCurrentOwnerName = require('getCurrentOwnerName'); var invariant = require('invariant'); var warning = require('warning'); @@ -69,7 +69,7 @@ var ReactDOMTextarea = { ReactControlledValuePropTypes.checkPropTypes( 'textarea', props, - getCurrentOwnerName() + getCurrentFiberOwnerName() ); if ( props.value !== undefined && diff --git a/src/renderers/dom/shared/CSSPropertyOperations.js b/src/renderers/dom/shared/CSSPropertyOperations.js index 4843ed0dac59..c970007bf7a6 100644 --- a/src/renderers/dom/shared/CSSPropertyOperations.js +++ b/src/renderers/dom/shared/CSSPropertyOperations.js @@ -13,7 +13,6 @@ var CSSProperty = require('CSSProperty'); var ExecutionEnvironment = require('ExecutionEnvironment'); -var ReactInstrumentation = require('ReactInstrumentation'); var camelizeStyleName = require('camelizeStyleName'); var dangerousStyleValue = require('dangerousStyleValue'); @@ -22,6 +21,10 @@ var hyphenateStyleName = require('hyphenateStyleName'); var memoizeStringOnly = require('memoizeStringOnly'); var warning = require('warning'); +if (__DEV__) { + var { getCurrentFiberOwnerName } = require('ReactDebugCurrentFiber'); +} + var processStyleName = memoizeStringOnly(function(styleName) { return hyphenateStyleName(styleName); }); @@ -114,11 +117,18 @@ if (__DEV__) { }; var checkRenderMessage = function(owner) { - if (owner) { - var name = getComponentName(owner); - if (name) { - return ' Check the render method of `' + name + '`.'; - } + var ownerName; + if (owner != null) { + // Stack passes the owner manually all the way to CSSPropertyOperations. + ownerName = getComponentName(owner); + } else { + // Fiber doesn't pass it but uses ReactDebugCurrentFiber to track it. + // It is only enabled in development and tracks host components too. + ownerName = getCurrentFiberOwnerName(); + // TODO: also report the stack. + } + if (ownerName) { + return ' Check the render method of `' + ownerName + '`.'; } return ''; }; @@ -193,14 +203,6 @@ var CSSPropertyOperations = { * @param {ReactDOMComponent} component */ setValueForStyles: function(node, styles, component) { - if (__DEV__) { - ReactInstrumentation.debugTool.onHostOperation({ - instanceID: component._debugID, - type: 'update styles', - payload: styles, - }); - } - var style = node.style; for (var styleName in styles) { if (!styles.hasOwnProperty(styleName)) { diff --git a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js index 6c5661e6663d..5ef4e545a5eb 100644 --- a/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js +++ b/src/renderers/dom/shared/__tests__/ReactDOMComponent-test.js @@ -848,6 +848,33 @@ describe('ReactDOMComponent', () => { ); }); + it('should include owner rather than parent in warnings', () => { + var container = document.createElement('div'); + + function Parent(props) { + return props.children; + } + function Owner() { + // We're using the input dangerouslySetInnerHTML invariant but the + // exact error doesn't matter as long as we have a way to verify + // that warnings and invariants contain owner rather than parent name. + return ( + + + + ); + } + + expect(function() { + ReactDOM.render( + , + container + ); + }).toThrowError( + 'This DOM node was rendered by `Owner`.' + ); + }); + it('should emit a warning once for a named custom component using shady DOM', () => { if (ReactDOMFeatureFlags.useCreateElement) { spyOn(console, 'error'); @@ -1021,7 +1048,7 @@ describe('ReactDOMComponent', () => { ReactDOM.render(, container); }).toThrowError( 'input is a void element tag and must neither have `children` ' + - 'nor use `dangerouslySetInnerHTML`. Check the render method of X.' + 'nor use `dangerouslySetInnerHTML`. This DOM node was rendered by `X`.' ); }); @@ -1408,6 +1435,13 @@ describe('ReactDOMComponent', () => { }); it('should warn about class', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument(React.createElement('div', {class: 'muffins'})); + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain('className'); + }); + + it('should warn about class (ssr)', () => { spyOn(console, 'error'); ReactDOMServer.renderToString(React.createElement('div', {class: 'muffins'})); expectDev(console.error.calls.count()).toBe(1); @@ -1426,7 +1460,37 @@ describe('ReactDOMComponent', () => { expectDev(console.error.calls.count()).toBe(2); }); + it('should warn about props that are no longer supported (ssr)', () => { + spyOn(console, 'error'); + ReactDOMServer.renderToString(
); + expectDev(console.error.calls.count()).toBe(0); + + ReactDOMServer.renderToString(
{}} />); + expectDev(console.error.calls.count()).toBe(1); + + ReactDOMServer.renderToString(
{}} />); + expectDev(console.error.calls.count()).toBe(2); + }); + it('gives source code refs for unknown prop warning', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument(
); + ReactTestUtils.renderIntoDocument(); + expectDev(console.error.calls.count()).toBe(2); + expect( + normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]) + ).toBe( + 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)' + ); + expect( + normalizeCodeLocInfo(console.error.calls.argsFor(1)[0]) + ).toBe( + 'Warning: Unknown event handler property onclick. Did you mean ' + + '`onClick`?\n in input (at **)' + ); + }); + + it('gives source code refs for unknown prop warning (ssr)', () => { spyOn(console, 'error'); ReactDOMServer.renderToString(
); ReactDOMServer.renderToString(); @@ -1448,20 +1512,47 @@ describe('ReactDOMComponent', () => { spyOn(console, 'error'); var container = document.createElement('div'); - ReactDOMServer.renderToString(
, container); + ReactTestUtils.renderIntoDocument(
, container); expectDev(console.error.calls.count()).toBe(0); - ReactDOMServer.renderToString(
, container); + ReactTestUtils.renderIntoDocument(
, container); expectDev(console.error.calls.count()).toBe(1); expect( normalizeCodeLocInfo(console.error.calls.argsFor(0)[0]) ).toBe( 'Warning: Unknown DOM property class. Did you mean className?\n in div (at **)' ); + }); + + it('gives source code refs for unknown prop warning for exact elements', () => { + spyOn(console, 'error'); + ReactTestUtils.renderIntoDocument( +
+
+
+
+
+
+
+ ); + + expectDev(console.error.calls.count()).toBe(2); + + expectDev(console.error.calls.argsFor(0)[0]).toContain('className'); + var matches = console.error.calls.argsFor(0)[0].match(/.*\(.*:(\d+)\).*/); + var previousLine = matches[1]; + + expectDev(console.error.calls.argsFor(1)[0]).toContain('onClick'); + matches = console.error.calls.argsFor(1)[0].match(/.*\(.*:(\d+)\).*/); + var currentLine = matches[1]; + + //verify line number has a proper relative difference, + //since hard coding the line number would make test too brittle + expect(parseInt(previousLine, 10) + 2).toBe(parseInt(currentLine, 10)); }); - it('gives source code refs for unknown prop warning for exact elements ', () => { + it('gives source code refs for unknown prop warning for exact elements (ssr)', () => { spyOn(console, 'error'); ReactDOMServer.renderToString( @@ -1489,7 +1580,58 @@ describe('ReactDOMComponent', () => { expect(parseInt(previousLine, 10) + 2).toBe(parseInt(currentLine, 10)); }); - it('gives source code refs for unknown prop warning for exact elements in composition ', () => { + it('gives source code refs for unknown prop warning for exact elements in composition', () => { + spyOn(console, 'error'); + var container = document.createElement('div'); + + class Parent extends React.Component { + render() { + return
; + } + } + + class Child1 extends React.Component { + render() { + return
Child1
; + } + } + + class Child2 extends React.Component { + render() { + return
Child2
; + } + } + + class Child3 extends React.Component { + render() { + return
Child3
; + } + } + + class Child4 extends React.Component { + render() { + return
Child4
; + } + } + + ReactTestUtils.renderIntoDocument(, container); + + expectDev(console.error.calls.count()).toBe(2); + + expectDev(console.error.calls.argsFor(0)[0]).toContain('className'); + var matches = console.error.calls.argsFor(0)[0].match(/.*\(.*:(\d+)\).*/); + var previousLine = matches[1]; + + expectDev(console.error.calls.argsFor(1)[0]).toContain('onClick'); + matches = console.error.calls.argsFor(1)[0].match(/.*\(.*:(\d+)\).*/); + var currentLine = matches[1]; + + //verify line number has a proper relative difference, + //since hard coding the line number would make test too brittle + expect(parseInt(previousLine, 10) + 12).toBe(parseInt(currentLine, 10)); + }); + + it('gives source code refs for unknown prop warning for exact elements in composition (ssr)', () => { spyOn(console, 'error'); var container = document.createElement('div'); @@ -1556,5 +1698,22 @@ describe('ReactDOMComponent', () => { 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input' ); }); + + it('should suggest property name if available (ssr)', () => { + spyOn(console, 'error'); + + ReactDOMServer.renderToString(React.createElement('label', {for: 'test'})); + ReactDOMServer.renderToString(React.createElement('input', {type: 'text', autofocus: true})); + + expectDev(console.error.calls.count()).toBe(2); + + expectDev(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: Unknown DOM property for. Did you mean htmlFor?\n in label' + ); + + expectDev(console.error.calls.argsFor(1)[0]).toBe( + 'Warning: Unknown DOM property autofocus. Did you mean autoFocus?\n in input' + ); + }); }); }); diff --git a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js index 6e2fdcfa363e..a470e9afec24 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMInvalidARIAHook.js @@ -13,12 +13,23 @@ var DOMProperty = require('DOMProperty'); var ReactComponentTreeHook = require('ReactComponentTreeHook'); +var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var warning = require('warning'); var warnedProperties = {}; var rARIA = new RegExp('^(aria)-[' + DOMProperty.ATTRIBUTE_NAME_CHAR + ']*$'); +function getStackAddendum(debugID) { + if (debugID != null) { + // This can only happen on Stack + return ReactComponentTreeHook.getStackAddendumByID(debugID); + } else { + // This can only happen on Fiber + return ReactDebugCurrentFiber.getCurrentFiberStackAddendum(); + } +} + function validateProperty(tagName, name, debugID) { if ( warnedProperties.hasOwnProperty(name) @@ -47,7 +58,7 @@ function validateProperty(tagName, name, debugID) { 'Unknown ARIA attribute %s. Did you mean %s?%s', name, standardName, - ReactComponentTreeHook.getStackAddendumByID(debugID) + getStackAddendum(debugID) ); warnedProperties[name] = true; return true; @@ -57,11 +68,11 @@ function validateProperty(tagName, name, debugID) { return true; } -function warnInvalidARIAProps(debugID, element) { +function warnInvalidARIAProps(type, props, debugID) { const invalidProps = []; - for (var key in element.props) { - var isValid = validateProperty(element.type, key, debugID); + for (var key in props) { + var isValid = validateProperty(type, key, debugID); if (!isValid) { invalidProps.push(key); } @@ -77,8 +88,8 @@ function warnInvalidARIAProps(debugID, element) { 'Invalid aria prop %s on <%s> tag. ' + 'For details, see https://fb.me/invalid-aria-prop%s', unknownPropString, - element.type, - ReactComponentTreeHook.getStackAddendumByID(debugID) + type, + getStackAddendum(debugID) ); } else if (invalidProps.length > 1) { warning( @@ -86,32 +97,31 @@ function warnInvalidARIAProps(debugID, element) { 'Invalid aria props %s on <%s> tag. ' + 'For details, see https://fb.me/invalid-aria-prop%s', unknownPropString, - element.type, - ReactComponentTreeHook.getStackAddendumByID(debugID) + type, + getStackAddendum(debugID) ); } } -function handleElement(debugID, element) { - if (element == null || typeof element.type !== 'string') { +function validateProperties(type, props, debugID /* Stack only */) { + if (type.indexOf('-') >= 0 || props.is) { return; } - if (element.type.indexOf('-') >= 0 || element.props.is) { - return; - } - - warnInvalidARIAProps(debugID, element); + warnInvalidARIAProps(type, props, debugID); } var ReactDOMInvalidARIAHook = { + // Fiber + validateProperties, + // Stack onBeforeMountComponent(debugID, element) { - if (__DEV__) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); } }, onBeforeUpdateComponent(debugID, element) { - if (__DEV__) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); } }, }; diff --git a/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js b/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js index 959db6bf26cc..564f6e1abd7e 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMNullInputValuePropHook.js @@ -12,26 +12,34 @@ 'use strict'; var ReactComponentTreeHook = require('ReactComponentTreeHook'); +var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var warning = require('warning'); var didWarnValueNull = false; -function handleElement(debugID, element) { - if (element == null) { - return; +function getStackAddendum(debugID) { + if (debugID != null) { + // This can only happen on Stack + return ReactComponentTreeHook.getStackAddendumByID(debugID); + } else { + // This can only happen on Fiber + return ReactDebugCurrentFiber.getCurrentFiberStackAddendum(); } - if (element.type !== 'input' && element.type !== 'textarea' && element.type !== 'select') { +} + +function validateProperties(type, props, debugID /* Stack only */) { + if (type !== 'input' && type !== 'textarea' && type !== 'select') { return; } - if (element.props != null && element.props.value === null && !didWarnValueNull) { + if (props != null && props.value === null && !didWarnValueNull) { warning( false, '`value` prop on `%s` should not be null. ' + 'Consider using the empty string to clear the component or `undefined` ' + 'for uncontrolled components.%s', - element.type, - ReactComponentTreeHook.getStackAddendumByID(debugID) + type, + getStackAddendum(debugID) ); didWarnValueNull = true; @@ -39,11 +47,18 @@ function handleElement(debugID, element) { } var ReactDOMNullInputValuePropHook = { + // Fiber + validateProperties, + // Stack onBeforeMountComponent(debugID, element) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); + } }, onBeforeUpdateComponent(debugID, element) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); + } }, }; diff --git a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js index 5f409cddb376..6462875f6591 100644 --- a/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js +++ b/src/renderers/dom/shared/hooks/ReactDOMUnknownPropertyHook.js @@ -14,9 +14,20 @@ var DOMProperty = require('DOMProperty'); var EventPluginRegistry = require('EventPluginRegistry'); var ReactComponentTreeHook = require('ReactComponentTreeHook'); +var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); var warning = require('warning'); +function getStackAddendum(debugID) { + if (debugID != null) { + // This can only happen on Stack + return ReactComponentTreeHook.getStackAddendumByID(debugID); + } else { + // This can only happen on Fiber + return ReactDebugCurrentFiber.getCurrentFiberStackAddendum(); + } +} + if (__DEV__) { var reactProps = { children: true, @@ -71,7 +82,7 @@ if (__DEV__) { 'Unknown DOM property %s. Did you mean %s?%s', name, standardName, - ReactComponentTreeHook.getStackAddendumByID(debugID) + getStackAddendum(debugID) ); return true; } else if (registrationName != null) { @@ -80,7 +91,7 @@ if (__DEV__) { 'Unknown event handler property %s. Did you mean `%s`?%s', name, registrationName, - ReactComponentTreeHook.getStackAddendumByID(debugID) + getStackAddendum(debugID) ); return true; } else { @@ -93,10 +104,10 @@ if (__DEV__) { }; } -var warnUnknownProperties = function(debugID, element) { +var warnUnknownProperties = function(type, props, debugID) { var unknownProps = []; - for (var key in element.props) { - var isValid = validateProperty(element.type, key, debugID); + for (var key in props) { + var isValid = validateProperty(type, key, debugID); if (!isValid) { unknownProps.push(key); } @@ -112,8 +123,8 @@ var warnUnknownProperties = function(debugID, element) { 'Unknown prop %s on <%s> tag. Remove this prop from the element. ' + 'For details, see https://fb.me/react-unknown-prop%s', unknownPropString, - element.type, - ReactComponentTreeHook.getStackAddendumByID(debugID) + type, + getStackAddendum(debugID) ); } else if (unknownProps.length > 1) { warning( @@ -121,28 +132,32 @@ var warnUnknownProperties = function(debugID, element) { 'Unknown props %s on <%s> tag. Remove these props from the element. ' + 'For details, see https://fb.me/react-unknown-prop%s', unknownPropString, - element.type, - ReactComponentTreeHook.getStackAddendumByID(debugID) + type, + getStackAddendum(debugID) ); } }; -function handleElement(debugID, element) { - if (element == null || typeof element.type !== 'string') { +function validateProperties(type, props, debugID /* Stack only */) { + if (type.indexOf('-') >= 0 || props.is) { return; } - if (element.type.indexOf('-') >= 0 || element.props.is) { - return; - } - warnUnknownProperties(debugID, element); + warnUnknownProperties(type, props, debugID); } var ReactDOMUnknownPropertyHook = { + // Fiber + validateProperties, + // Stack onBeforeMountComponent(debugID, element) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); + } }, onBeforeUpdateComponent(debugID, element) { - handleElement(debugID, element); + if (__DEV__ && element != null && typeof element.type === 'string') { + validateProperties(element.type, element.props, debugID); + } }, }; diff --git a/src/renderers/dom/stack/client/ReactDOMComponent.js b/src/renderers/dom/stack/client/ReactDOMComponent.js index b23a736b083c..fbfd8ccf0ad8 100644 --- a/src/renderers/dom/stack/client/ReactDOMComponent.js +++ b/src/renderers/dom/stack/client/ReactDOMComponent.js @@ -88,10 +88,7 @@ function assertValidProps(component, props) { '%s is a void element tag and must neither have `children` nor ' + 'use `dangerouslySetInnerHTML`.%s', component._tag, - component._currentElement._owner ? - ' Check the render method of ' + - component._currentElement._owner.getName() + '.' : - '' + getDeclarationErrorAddendum(component) ); } if (props.dangerouslySetInnerHTML != null) { @@ -1029,6 +1026,13 @@ ReactDOMComponent.Mixin = { } } if (styleUpdates) { + if (__DEV__) { + ReactInstrumentation.debugTool.onHostOperation({ + instanceID: this._debugID, + type: 'update styles', + payload: styleUpdates, + }); + } CSSPropertyOperations.setValueForStyles( getNode(this), styleUpdates, diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index e7ee06f88c41..028490ee83cd 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -12,6 +12,7 @@ 'use strict'; +import type { ReactElement } from 'ReactElementType'; import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { ReactPortal } from 'ReactPortal'; import type { Fiber } from 'ReactFiber'; @@ -36,6 +37,11 @@ var emptyObject = require('emptyObject'); var getIteratorFn = require('getIteratorFn'); var invariant = require('invariant'); +if (__DEV__) { + var { getCurrentFiberStackAddendum } = require('ReactDebugCurrentFiber'); + var warning = require('warning'); +} + const { cloneFiber, createFiberFromElement, @@ -68,7 +74,7 @@ const { Deletion, } = ReactTypeOfSideEffect; -function coerceRef(current: ?Fiber, element: ReactElement) { +function coerceRef(current: ?Fiber, element: ReactElement) { let mixedRef = element.ref; if (mixedRef != null && typeof mixedRef !== 'function') { if (element._owner) { @@ -256,7 +262,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function updateElement( returnFiber : Fiber, current : ?Fiber, - element : ReactElement, + element : ReactElement, priority : PriorityLevel ) : Fiber { if (current == null || current.type !== element.type) { @@ -271,6 +277,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing.ref = coerceRef(current, element); existing.pendingProps = element.props; existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } return existing; } } @@ -536,6 +546,48 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return null; } + function warnOnDuplicateKey( + child : mixed, + knownKeys : Set | null + ) : Set | null { + if (__DEV__) { + if (typeof child !== 'object' || child == null) { + return knownKeys; + } + switch (child.$$typeof) { + case REACT_ELEMENT_TYPE: + case REACT_COROUTINE_TYPE: + case REACT_YIELD_TYPE: + case REACT_PORTAL_TYPE: + const key = child.key; + if (typeof key !== 'string') { + break; + } + if (knownKeys == null) { + knownKeys = new Set(); + knownKeys.add(key); + break; + } + if (!knownKeys.has(key)) { + knownKeys.add(key); + break; + } + warning( + false, + 'Encountered two children with the same key, ' + + '`%s`. Child keys must be unique; when two children share a key, ' + + 'only the first child will be used.%s', + key, + getCurrentFiberStackAddendum() + ); + break; + default: + break; + } + } + return knownKeys; + } + function reconcileChildrenArray( returnFiber : Fiber, currentFirstChild : ?Fiber, @@ -561,6 +613,15 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // If you change this code, also update reconcileChildrenIterator() which // uses the same algorithm. + if (__DEV__) { + // First, validate keys. + let knownKeys = null; + for (let i = 0; i < newChildren.length; i++) { + const child = newChildren[i]; + knownKeys = warnOnDuplicateKey(child, knownKeys); + } + } + let resultingFirstChild : ?Fiber = null; let previousNewFiber : ?Fiber = null; @@ -691,12 +752,37 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function reconcileChildrenIterator( returnFiber : Fiber, currentFirstChild : ?Fiber, - newChildren : Iterator<*>, + newChildrenIterable : Iterable<*>, priority : PriorityLevel) : ?Fiber { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. + const iteratorFn = getIteratorFn(newChildrenIterable); + if (typeof iteratorFn !== 'function') { + throw new Error('An object is not an iterable.'); + } + + if (__DEV__) { + // First, validate keys. + // We'll get a different iterator later for the main pass. + const newChildren = iteratorFn.call(newChildrenIterable); + if (newChildren == null) { + throw new Error('An iterable object provided no iterator.'); + } + let knownKeys = null; + let step = newChildren.next(); + for (; !step.done; step = newChildren.next()) { + const child = step.value; + knownKeys = warnOnDuplicateKey(child, knownKeys); + } + } + + const newChildren = iteratorFn.call(newChildrenIterable); + if (newChildren == null) { + throw new Error('An iterable object provided no iterator.'); + } + let resultingFirstChild : ?Fiber = null; let previousNewFiber : ?Fiber = null; @@ -854,7 +940,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function reconcileSingleElement( returnFiber : Fiber, currentFirstChild : ?Fiber, - element : ReactElement, + element : ReactElement, priority : PriorityLevel ) : Fiber { const key = element.key; @@ -869,6 +955,10 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existing.ref = coerceRef(child, element); existing.pendingProps = element.props; existing.return = returnFiber; + if (__DEV__) { + existing._debugSource = element._source; + existing._debugOwner = element._owner; + } return existing; } else { deleteRemainingChildren(returnFiber, child); @@ -1061,16 +1151,11 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { ); } - const iteratorFn = getIteratorFn(newChild); - if (iteratorFn) { - const iterator = iteratorFn.call(newChild); - if (iterator == null) { - throw new Error('An iterable object provided no iterator.'); - } + if (getIteratorFn(newChild)) { return reconcileChildrenIterator( returnFiber, currentFirstChild, - iterator, + newChild, priority ); } diff --git a/src/renderers/shared/fiber/ReactDebugCurrentFiber.js b/src/renderers/shared/fiber/ReactDebugCurrentFiber.js new file mode 100644 index 000000000000..6b25a05ce032 --- /dev/null +++ b/src/renderers/shared/fiber/ReactDebugCurrentFiber.js @@ -0,0 +1,55 @@ +/** + * Copyright 2013-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + * @providesModule ReactDebugCurrentFiber + * @flow + */ + +'use strict'; + +import type { Fiber } from 'ReactFiber'; + +if (__DEV__) { + var getComponentName = require('getComponentName'); + var { getStackAddendumByWorkInProgressFiber } = require('ReactComponentTreeHook'); +} + +function getCurrentFiberOwnerName() : string | null { + if (__DEV__) { + const fiber = ReactDebugCurrentFiber.current; + if (fiber == null) { + return null; + } + if (fiber._debugOwner == null) { + return null; + } + return getComponentName(fiber._debugOwner); + } + return null; +} + +function getCurrentFiberStackAddendum() : string | null { + if (__DEV__) { + const fiber = ReactDebugCurrentFiber.current; + if (fiber == null) { + return null; + } + // Safe because if current fiber exists, we are reconciling, + // and it is guaranteed to be the work-in-progress version. + return getStackAddendumByWorkInProgressFiber(fiber); + } + return null; +} + +var ReactDebugCurrentFiber = { + current: (null : Fiber | null), + getCurrentFiberOwnerName, + getCurrentFiberStackAddendum, +}; + +module.exports = ReactDebugCurrentFiber; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 7d3097ffaf4f..8873e2b46c22 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -12,6 +12,8 @@ 'use strict'; +import type { ReactElement, Source } from 'ReactElementType'; +import type { ReactInstance, DebugID } from 'ReactInstanceType'; import type { ReactFragment } from 'ReactTypes'; import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { ReactPortal } from 'ReactPortal'; @@ -46,6 +48,11 @@ var invariant = require('invariant'); // A Fiber is work on a Component that needs to be done or was done. There can // be more than one per component. export type Fiber = { + // __DEV__ only + _debugID ?: DebugID, + _debugSource ?: Source | null, + _debugOwner ?: Fiber | ReactInstance | null, // Stack compatible + // These first fields are conceptually members of an Instance. This used to // be split into a separate type and intersected with the other Fiber fields, // but until Flow fixes its intersection bugs, we've merged them into a @@ -145,7 +152,7 @@ export type Fiber = { }; if (__DEV__) { - var debugCounter = 0; + var debugCounter = 1; } // This is a constructor of a POJO instead of a constructor function for a few @@ -162,7 +169,7 @@ if (__DEV__) { // 5) It should be easy to port this to a C struct and keep a C implementation // compatible. var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { - var fiber = { + var fiber : Fiber = { // Instance @@ -204,9 +211,13 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { alternate: null, }; + if (__DEV__) { - (fiber : any)._debugID = debugCounter++; + fiber._debugID = debugCounter++; + fiber._debugSource = null; + fiber._debugOwner = null; } + return fiber; }; @@ -266,6 +277,12 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi alt.memoizedProps = fiber.memoizedProps; alt.memoizedState = fiber.memoizedState; + if (__DEV__) { + alt._debugID = fiber._debugID; + alt._debugSource = fiber._debugSource; + alt._debugOwner = fiber._debugOwner; + } + return alt; }; @@ -274,11 +291,16 @@ exports.createHostRootFiber = function() : Fiber { return fiber; }; -exports.createFiberFromElement = function(element : ReactElement<*>, priorityLevel : PriorityLevel) : Fiber { -// $FlowFixMe: ReactElement.key is currently defined as ?string but should be defined as null | string in Flow. +exports.createFiberFromElement = function(element : ReactElement, priorityLevel : PriorityLevel) : Fiber { const fiber = createFiberFromElementType(element.type, element.key); fiber.pendingProps = element.props; fiber.pendingWorkPriority = priorityLevel; + + if (__DEV__) { + fiber._debugSource = element._source; + fiber._debugOwner = element._owner; + } + return fiber; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ca68c0d01bab..bc98949e6fce 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -60,6 +60,10 @@ var { var ReactCurrentOwner = require('ReactCurrentOwner'); var ReactFiberClassComponent = require('ReactFiberClassComponent'); +if (__DEV__) { + var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); +} + module.exports = function( config : HostConfig, hostContext : HostContext, @@ -452,6 +456,10 @@ module.exports = function( return bailoutOnLowPriority(current, workInProgress); } + if (__DEV__) { + ReactDebugCurrentFiber.current = workInProgress; + } + // If we don't bail out, we're going be recomputing our children so we need // to drop our effect list. workInProgress.firstEffect = null; diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 79a6a9ebc9ef..79595dd152d7 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -180,6 +180,14 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', name ); + const hasMutatedProps = instance.props !== workInProgress.pendingProps; + warning( + instance.props === undefined || !hasMutatedProps, + '%s(...): When calling super() in `%s`, make sure to pass ' + + 'up the same props that your component\'s constructor was passed.', + name, + name + ); } const state = instance.state; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index b8ffe3132f0f..d0a6ab217d75 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -44,6 +44,10 @@ var { Callback, } = ReactTypeOfSideEffect; +if (__DEV__) { + var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); +} + module.exports = function( config : HostConfig, hostContext : HostContext, @@ -167,6 +171,10 @@ module.exports = function( } function completeWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { + if (__DEV__) { + ReactDebugCurrentFiber.current = workInProgress; + } + switch (workInProgress.tag) { case FunctionalComponent: workInProgress.memoizedProps = workInProgress.pendingProps; diff --git a/src/renderers/shared/fiber/ReactFiberContext.js b/src/renderers/shared/fiber/ReactFiberContext.js index 4fdb8e2ec31d..76e8ed568f62 100644 --- a/src/renderers/shared/fiber/ReactFiberContext.js +++ b/src/renderers/shared/fiber/ReactFiberContext.js @@ -40,8 +40,8 @@ function getUnmaskedContext() { return contextStack[index]; } -exports.getMaskedContext = function(fiber : Fiber) { - const type = fiber.type; +exports.getMaskedContext = function(workInProgress : Fiber) { + const type = workInProgress.type; const contextTypes = type.contextTypes; if (!contextTypes) { return emptyObject; @@ -54,9 +54,8 @@ exports.getMaskedContext = function(fiber : Fiber) { } if (__DEV__) { - const name = getComponentName(fiber); - const debugID = 0; // TODO: pass a real ID - checkReactTypeSpec(contextTypes, context, 'context', name, null, debugID); + const name = getComponentName(workInProgress); + checkReactTypeSpec(contextTypes, context, 'context', name, null, workInProgress); } return context; @@ -91,7 +90,7 @@ exports.pushTopLevelContextObject = function(context : Object, didChange : boole didPerformWorkStack[index] = didChange; }; -function processChildContext(fiber : Fiber, parentContext : Object): Object { +function processChildContext(fiber : Fiber, parentContext : Object, isReconciling : boolean): Object { const instance = fiber.stateNode; const childContextTypes = fiber.type.childContextTypes; const childContext = instance.getChildContext(); @@ -105,15 +104,20 @@ function processChildContext(fiber : Fiber, parentContext : Object): Object { } if (__DEV__) { const name = getComponentName(fiber); - const debugID = 0; // TODO: pass a real ID - checkReactTypeSpec(childContextTypes, childContext, 'childContext', name, null, debugID); + // We can only provide accurate element stacks if we pass work-in-progress tree + // during the begin or complete phase. However currently this function is also + // called from unstable_renderSubtree legacy implementation. In this case it unsafe to + // assume anything about the given fiber. We won't pass it down if we aren't sure. + // TODO: remove this hack when we delete unstable_renderSubtree in Fiber. + const workInProgress = isReconciling ? fiber : null; + checkReactTypeSpec(childContextTypes, childContext, 'childContext', name, null, workInProgress); } return {...parentContext, ...childContext}; } exports.processChildContext = processChildContext; -exports.pushContextProvider = function(fiber : Fiber, didPerformWork : boolean) : void { - const instance = fiber.stateNode; +exports.pushContextProvider = function(workInProgress : Fiber, didPerformWork : boolean) : void { + const instance = workInProgress.stateNode; const memoizedMergedChildContext = instance.__reactInternalMemoizedMergedChildContext; const canReuseMergedChildContext = !didPerformWork && memoizedMergedChildContext != null; @@ -121,7 +125,7 @@ exports.pushContextProvider = function(fiber : Fiber, didPerformWork : boolean) if (canReuseMergedChildContext) { mergedContext = memoizedMergedChildContext; } else { - mergedContext = processChildContext(fiber, getUnmaskedContext()); + mergedContext = processChildContext(workInProgress, getUnmaskedContext(), true); instance.__reactInternalMemoizedMergedChildContext = mergedContext; } diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 571d9aa921d1..582d7946e322 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -91,7 +91,7 @@ export type Reconciler = { getContextForSubtree._injectFiber(function(fiber : Fiber) { const parentContext = findCurrentUnmaskedContext(fiber); return isContextProvider(fiber) ? - processChildContext(fiber, parentContext) : + processChildContext(fiber, parentContext, false) : parentContext; }); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4d50ac0a41f1..7bf2f7d7aa56 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -59,6 +59,7 @@ var { if (__DEV__) { var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); + var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); } var timeHeuristicForUnitOfWork = 1; @@ -186,6 +187,10 @@ module.exports = function(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig(config : HostConfig { ReactTestUtils = require('ReactTestUtils'); }); - it('warns for duplicated keys', () => { + function createIterable(array) { + return { + '@@iterator': function() { + var i = 0; + return { + next() { + const next = { + value: i < array.length ? array[i] : undefined, + done: i === array.length, + }; + i++; + return next; + }, + }; + }, + }; + } + + it('warns for duplicated array keys', () => { spyOn(console, 'error'); class Component extends React.Component { @@ -46,7 +64,7 @@ describe('ReactChildReconciler', () => { ); }); - it('warns for duplicated keys with component stack info', () => { + it('warns for duplicated array keys with component stack info', () => { spyOn(console, 'error'); class Component extends React.Component { @@ -70,8 +88,59 @@ describe('ReactChildReconciler', () => { ReactTestUtils.renderIntoDocument(); expectDev(console.error.calls.count()).toBe(1); - expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: flattenChildren(...): ' + + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain( + 'Encountered two children with the same key, `1`. ' + + 'Child keys must be unique; when two children share a key, ' + + 'only the first child will be used.\n' + + ' in div (at **)\n' + + ' in Component (at **)\n' + + ' in Parent (at **)\n' + + ' in GrandParent (at **)' + ); + }); + + it('warns for duplicated iterable keys', () => { + spyOn(console, 'error'); + + class Component extends React.Component { + render() { + return
{createIterable([
,
])}
; + } + } + + ReactTestUtils.renderIntoDocument(); + + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toContain( + 'Child keys must be unique; when two children share a key, only the first child will be used.' + ); + }); + + it('warns for duplicated iterable keys with component stack info', () => { + spyOn(console, 'error'); + + class Component extends React.Component { + render() { + return
{createIterable([
,
])}
; + } + } + + class Parent extends React.Component { + render() { + return React.cloneElement(this.props.child); + } + } + + class GrandParent extends React.Component { + render() { + return } />; + } + } + + ReactTestUtils.renderIntoDocument(); + + expectDev(console.error.calls.count()).toBe(1); + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain( 'Encountered two children with the same key, `1`. ' + 'Child keys must be unique; when two children share a key, ' + 'only the first child will be used.\n' + diff --git a/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js b/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js index e2d14a7cc3c9..3e005fd1afca 100644 --- a/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js +++ b/src/renderers/shared/shared/__tests__/ReactMultiChild-test.js @@ -152,7 +152,7 @@ describe('ReactMultiChild', () => { expect(mockUnmount.mock.calls.length).toBe(1); }); - it('should warn for duplicated keys with component stack info', () => { + it('should warn for duplicated array keys with component stack info', () => { spyOn(console, 'error'); var container = document.createElement('div'); @@ -186,8 +186,70 @@ describe('ReactMultiChild', () => { ); expectDev(console.error.calls.count()).toBe(1); - expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toBe( - 'Warning: flattenChildren(...): ' + + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain( + 'Encountered two children with the same key, `1`. ' + + 'Child keys must be unique; when two children share a key, ' + + 'only the first child will be used.\n' + + ' in div (at **)\n' + + ' in WrapperComponent (at **)\n' + + ' in div (at **)\n' + + ' in Parent (at **)' + ); + }); + + it('should warn for duplicated iterable keys with component stack info', () => { + spyOn(console, 'error'); + + var container = document.createElement('div'); + + class WrapperComponent extends React.Component { + render() { + return
{this.props.children}
; + } + } + + class Parent extends React.Component { + render() { + return ( +
+ + {this.props.children} + +
+ ); + } + } + + function createIterable(array) { + return { + '@@iterator': function() { + var i = 0; + return { + next() { + const next = { + value: i < array.length ? array[i] : undefined, + done: i === array.length, + }; + i++; + return next; + }, + }; + }, + }; + } + + ReactDOM.render( + {createIterable([
])}, + container + ); + + ReactDOM.render( + {createIterable([
,
])}, + container + ); + + expectDev(console.error.calls.count()).toBe(1); + expectDev(normalizeCodeLocInfo(console.error.calls.argsFor(0)[0])).toContain( 'Encountered two children with the same key, `1`. ' + 'Child keys must be unique; when two children share a key, ' + 'only the first child will be used.\n' + diff --git a/src/renderers/shared/shared/__tests__/refs-test.js b/src/renderers/shared/shared/__tests__/refs-test.js index f18b5870e161..a0ba0027061d 100644 --- a/src/renderers/shared/shared/__tests__/refs-test.js +++ b/src/renderers/shared/shared/__tests__/refs-test.js @@ -12,6 +12,7 @@ 'use strict'; var React = require('React'); +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var ReactTestUtils = require('ReactTestUtils'); /** @@ -334,12 +335,14 @@ describe('string refs between fiber and stack', () => { ReactDOMFiber.unmountComponentAtNode(container); expect(a.refs.span).toBe(undefined); expect(layerMounted).toBe(true); - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: You are using React DOM Fiber which is an experimental ' + - 'renderer. It is likely to have bugs, breaking changes and is ' + - 'unsupported.' - ); + if (!ReactDOMFeatureFlags.useFiber) { + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: You are using React DOM Fiber which is an experimental ' + + 'renderer. It is likely to have bugs, breaking changes and is ' + + 'unsupported.' + ); + } }); it('attaches, detaches from stack component with fiber layer', () => { @@ -379,11 +382,13 @@ describe('string refs between fiber and stack', () => { ReactDOM.unmountComponentAtNode(container); expect(a.refs.span).toBe(undefined); expect(layerMounted).toBe(true); - expectDev(console.error.calls.count()).toBe(1); - expectDev(console.error.calls.argsFor(0)[0]).toBe( - 'Warning: You are using React DOM Fiber which is an experimental ' + - 'renderer. It is likely to have bugs, breaking changes and is ' + - 'unsupported.' - ); + if (!ReactDOMFeatureFlags.useFiber) { + expectDev(console.error.calls.count()).toBe(1); + expectDev(console.error.calls.argsFor(0)[0]).toBe( + 'Warning: You are using React DOM Fiber which is an experimental ' + + 'renderer. It is likely to have bugs, breaking changes and is ' + + 'unsupported.' + ); + } }); }); diff --git a/src/renderers/shared/fiber/ReactTypeOfWork.js b/src/shared/ReactTypeOfWork.js similarity index 100% rename from src/renderers/shared/fiber/ReactTypeOfWork.js rename to src/shared/ReactTypeOfWork.js diff --git a/src/shared/types/checkReactTypeSpec.js b/src/shared/types/checkReactTypeSpec.js index c9315a349cdf..3b4c3c35ce50 100644 --- a/src/shared/types/checkReactTypeSpec.js +++ b/src/shared/types/checkReactTypeSpec.js @@ -44,7 +44,7 @@ var loggedTypeFailures = {}; * @param {string} location e.g. "prop", "context", "child context" * @param {string} componentName Name of the component for error messages. * @param {?object} element The React element that is being type-checked - * @param {?number} debugID The React component instance that is being type-checked + * @param {?number} workInProgressOrDebugID The React component instance that is being type-checked * @private */ function checkReactTypeSpec( @@ -53,7 +53,9 @@ function checkReactTypeSpec( location: ReactPropTypeLocations, componentName, element, - debugID, + // It is only safe to pass fiber if it is the work-in-progress version, and + // only during reconciliation (begin and complete phase). + workInProgressOrDebugID, ) { for (var typeSpecName in typeSpecs) { if (typeSpecs.hasOwnProperty(typeSpecName)) { @@ -99,8 +101,18 @@ function checkReactTypeSpec( if (!ReactComponentTreeHook) { ReactComponentTreeHook = require('ReactComponentTreeHook'); } - if (debugID !== null) { - componentStackInfo = ReactComponentTreeHook.getStackAddendumByID(debugID); + if (workInProgressOrDebugID != null) { + if (typeof workInProgressOrDebugID === 'number') { + // DebugID from Stack. + const debugID = workInProgressOrDebugID; + componentStackInfo = ReactComponentTreeHook.getStackAddendumByID(debugID); + } else if (typeof workInProgressOrDebugID.tag === 'number') { + // This is a Fiber. + // The stack will only be correct if this is a work in progress + // version and we're calling it during reconciliation. + const workInProgress = workInProgressOrDebugID; + componentStackInfo = ReactComponentTreeHook.getStackAddendumByWorkInProgressFiber(workInProgress); + } } else if (element !== null) { componentStackInfo = ReactComponentTreeHook.getCurrentStackAddendum(element); }