diff --git a/packages/react-dom/src/__tests__/ReactDOM-test.js b/packages/react-dom/src/__tests__/ReactDOM-test.js
index 97842417dc67..b37f4dd35f7a 100644
--- a/packages/react-dom/src/__tests__/ReactDOM-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOM-test.js
@@ -9,11 +9,20 @@
'use strict';
-let React = require('react');
-let ReactDOM = require('react-dom');
-const ReactTestUtils = require('react-dom/test-utils');
+let React;
+let ReactDOM;
+let ReactDOMServer;
+let ReactTestUtils;
describe('ReactDOM', () => {
+ beforeEach(() => {
+ jest.resetModules();
+ React = require('react');
+ ReactDOM = require('react-dom');
+ ReactDOMServer = require('react-dom/server');
+ ReactTestUtils = require('react-dom/test-utils');
+ });
+
// TODO: uncomment this test once we can run in phantom, which
// supports real submit events.
/*
@@ -446,4 +455,64 @@ describe('ReactDOM', () => {
global.requestAnimationFrame = previousRAF;
}
});
+
+ it('reports stacks with re-entrant renderToString() calls on the client', () => {
+ function Child2(props) {
+ return {props.children};
+ }
+
+ function App2() {
+ return (
+
+ {ReactDOMServer.renderToString()}
+
+ );
+ }
+
+ function Child() {
+ return (
+ {ReactDOMServer.renderToString()}
+ );
+ }
+
+ function ServerEntry() {
+ return ReactDOMServer.renderToString();
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const container = document.createElement('div');
+ expect(() => ReactDOM.render(, container)).toWarnDev([
+ // ReactDOM(App > div > span)
+ 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
+ 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in blink (at **)',
+ // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child) >>> ReactDOMServer(App2 > Child2 > span)
+ 'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in Child2 (at **)\n' +
+ ' in App2 (at **)',
+ // ReactDOM(App > div > ServerEntry) >>> ReactDOMServer(Child > span)
+ 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in Child (at **)',
+ // ReactDOM(App > div > font)
+ 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in font (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ ]);
+ });
});
diff --git a/packages/react-dom/src/__tests__/ReactServerRendering-test.js b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
index 6524b06671c8..a859e0c8f148 100644
--- a/packages/react-dom/src/__tests__/ReactServerRendering-test.js
+++ b/packages/react-dom/src/__tests__/ReactServerRendering-test.js
@@ -720,4 +720,61 @@ describe('ReactDOMServer', () => {
' in App (at **)',
]);
});
+
+ it('reports stacks with re-entrant renderToString() calls', () => {
+ function Child2(props) {
+ return {props.children};
+ }
+
+ function App2() {
+ return (
+
+ {ReactDOMServer.renderToString()}
+
+ );
+ }
+
+ function Child() {
+ return (
+ {ReactDOMServer.renderToString()}
+ );
+ }
+
+ function App() {
+ return (
+
+
+
+
+
+ );
+ }
+
+ expect(() => ReactDOMServer.renderToString()).toWarnDev([
+ // ReactDOMServer(App > div > span)
+ 'Invalid ARIA attribute `ariaTypo`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ // ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2) >>> ReactDOMServer(blink)
+ 'Invalid ARIA attribute `ariaTypo2`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in blink (at **)',
+ // ReactDOMServer(App > div > Child) >>> ReactDOMServer(App2 > Child2 > span)
+ 'Invalid ARIA attribute `ariaTypo3`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in Child2 (at **)\n' +
+ ' in App2 (at **)',
+ // ReactDOMServer(App > div > Child > span)
+ 'Invalid ARIA attribute `ariaTypo4`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in span (at **)\n' +
+ ' in Child (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ // ReactDOMServer(App > div > font)
+ 'Invalid ARIA attribute `ariaTypo5`. ARIA attributes follow the pattern aria-* and must be lowercase.\n' +
+ ' in font (at **)\n' +
+ ' in div (at **)\n' +
+ ' in App (at **)',
+ ]);
+ });
});
diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js
index f3c20cde346e..e2fec2d9a2cf 100644
--- a/packages/react-dom/src/server/ReactPartialRenderer.js
+++ b/packages/react-dom/src/server/ReactPartialRenderer.js
@@ -61,16 +61,20 @@ type FlatReactChildren = Array;
type toArrayType = (children: mixed) => FlatReactChildren;
const toArray = ((React.Children.toArray: any): toArrayType);
-let currentDebugStack;
-let currentDebugElementStack;
-
-let getStackAddendum = () => '';
+// This is only used in DEV.
+// Each entry is `this.stack` from a currently executing renderer instance.
+// (There may be more than one because ReactDOMServer is reentrant).
+// Each stack is an array of frames which may contain nested stacks of elements.
+let currentDebugStacks = [];
+
+let prevGetCurrentStackImpl = null;
+let getCurrentServerStackImpl = () => '';
let describeStackFrame = element => '';
let validatePropertiesInDevelopment = (type, props) => {};
-let setCurrentDebugStack = (stack: Array) => {};
+let pushCurrentDebugStack = (stack: Array) => {};
let pushElementToDebugStack = (element: ReactElement) => {};
-let resetCurrentDebugStack = () => {};
+let popCurrentDebugStack = () => {};
if (__DEV__) {
validatePropertiesInDevelopment = function(type, props) {
@@ -87,34 +91,55 @@ if (__DEV__) {
return describeComponentFrame(name, source, ownerName);
};
- currentDebugStack = null;
- currentDebugElementStack = null;
- setCurrentDebugStack = function(stack: Array) {
- const frame: Frame = stack[stack.length - 1];
- currentDebugElementStack = ((frame: any): FrameDev).debugElementStack;
- // We are about to enter a new composite stack, reset the array.
- currentDebugElementStack.length = 0;
- currentDebugStack = stack;
- ReactDebugCurrentFrame.getCurrentStack = getStackAddendum;
+ pushCurrentDebugStack = function(stack: Array) {
+ currentDebugStacks.push(stack);
+
+ if (currentDebugStacks.length === 1) {
+ // We are entering a server renderer.
+ // Remember the previous (e.g. client) global stack implementation.
+ prevGetCurrentStackImpl = ReactDebugCurrentFrame.getCurrentStack;
+ ReactDebugCurrentFrame.getCurrentStack = getCurrentServerStackImpl;
+ }
};
+
pushElementToDebugStack = function(element: ReactElement) {
- if (currentDebugElementStack !== null) {
- currentDebugElementStack.push(element);
- }
+ // For the innermost executing ReactDOMServer call,
+ const stack = currentDebugStacks[currentDebugStacks.length - 1];
+ // Take the innermost executing frame (e.g. ),
+ const frame: Frame = stack[stack.length - 1];
+ // and record that it has one more element associated with it.
+ ((frame: any): FrameDev).debugElementStack.push(element);
+ // We only need this because we tail-optimize single-element
+ // children and directly handle them in an inner loop instead of
+ // creating separate frames for them.
};
- resetCurrentDebugStack = function() {
- currentDebugElementStack = null;
- currentDebugStack = null;
- ReactDebugCurrentFrame.getCurrentStack = null;
+
+ popCurrentDebugStack = function() {
+ currentDebugStacks.pop();
+
+ if (currentDebugStacks.length === 0) {
+ // We are exiting the server renderer.
+ // Restore the previous (e.g. client) global stack implementation.
+ ReactDebugCurrentFrame.getCurrentStack = prevGetCurrentStackImpl;
+ prevGetCurrentStackImpl = null;
+ }
};
- getStackAddendum = function(): null | string {
- if (currentDebugStack === null) {
+
+ getCurrentServerStackImpl = function(): string {
+ if (currentDebugStacks.length === 0) {
+ // Nothing is currently rendering.
return '';
}
+ // ReactDOMServer is reentrant so there may be multiple calls at the same time.
+ // Take the frames from the innermost call which is the last in the array.
+ let frames = currentDebugStacks[currentDebugStacks.length - 1];
let stack = '';
- let debugStack = currentDebugStack;
- for (let i = debugStack.length - 1; i >= 0; i--) {
- const frame: Frame = debugStack[i];
+ // Go through every frame in the stack from the innermost one.
+ for (let i = frames.length - 1; i >= 0; i--) {
+ const frame: Frame = frames[i];
+ // Every frame might have more than one debug element stack entry associated with it.
+ // This is because single-child nesting doesn't create materialized frames.
+ // Instead it would push them through `pushElementToDebugStack()`.
let debugElementStack = ((frame: any): FrameDev).debugElementStack;
for (let ii = debugElementStack.length - 1; ii >= 0; ii--) {
stack += describeStackFrame(debugElementStack[ii]);
@@ -180,7 +205,7 @@ function createMarkupForStyles(styles): string | null {
const styleValue = styles[styleName];
if (__DEV__) {
if (!isCustomProperty) {
- warnValidStyle(styleName, styleValue, getStackAddendum);
+ warnValidStyle(styleName, styleValue, getCurrentServerStackImpl);
}
}
if (styleValue != null) {
@@ -305,7 +330,13 @@ function maskContext(type, context) {
function checkContextTypes(typeSpecs, values, location: string) {
if (__DEV__) {
- checkPropTypes(typeSpecs, values, location, 'Component', getStackAddendum);
+ checkPropTypes(
+ typeSpecs,
+ values,
+ location,
+ 'Component',
+ getCurrentServerStackImpl,
+ );
}
}
@@ -774,12 +805,18 @@ class ReactDOMServerRenderer {
}
const child = frame.children[frame.childIndex++];
if (__DEV__) {
- setCurrentDebugStack(this.stack);
- }
- out += this.render(child, frame.context, frame.domNamespace);
- if (__DEV__) {
- // TODO: Handle reentrant server render calls. This doesn't.
- resetCurrentDebugStack();
+ pushCurrentDebugStack(this.stack);
+ // We're starting work on this frame, so reset its inner stack.
+ ((frame: any): FrameDev).debugElementStack.length = 0;
+ try {
+ // Be careful! Make sure this matches the PROD path below.
+ out += this.render(child, frame.context, frame.domNamespace);
+ } finally {
+ popCurrentDebugStack();
+ }
+ } else {
+ // Be careful! Make sure this matches the DEV path above.
+ out += this.render(child, frame.context, frame.domNamespace);
}
}
return out;
@@ -1005,7 +1042,7 @@ class ReactDOMServerRenderer {
ReactControlledValuePropTypes.checkPropTypes(
'input',
props,
- getStackAddendum,
+ getCurrentServerStackImpl,
);
if (
@@ -1063,7 +1100,7 @@ class ReactDOMServerRenderer {
ReactControlledValuePropTypes.checkPropTypes(
'textarea',
props,
- getStackAddendum,
+ getCurrentServerStackImpl,
);
if (
props.value !== undefined &&
@@ -1124,7 +1161,7 @@ class ReactDOMServerRenderer {
ReactControlledValuePropTypes.checkPropTypes(
'select',
props,
- getStackAddendum,
+ getCurrentServerStackImpl,
);
for (let i = 0; i < valuePropNames.length; i++) {
@@ -1215,7 +1252,7 @@ class ReactDOMServerRenderer {
validatePropertiesInDevelopment(tag, props);
}
- assertValidProps(tag, props, getStackAddendum);
+ assertValidProps(tag, props, getCurrentServerStackImpl);
let out = createOpenTagMarkup(
element.type,