Bug fix: SSR setState in diff components don't mix (#12323)
Previously, the `queue` and `replace` arguments were leaking across loops even though they should be captured.
Sophie Alpert committed
Mar 3, 2018 at 10:27 UTC
1d220ce0b7bc7f4843cf90f1fe8933b9fcba9deb
2 files changed
+58
-32
packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js
+34
@@ -226,4 +226,38 @@ describe('ReactDOMServerLifecycles', () => {
226
ReactDOMServer.renderToString(<Component />);
227
expect(log).toEqual(['componentWillMount', 'UNSAFE_componentWillMount']);
228
});
229
+
230
+ it('tracks state updates across components', () => {
231
+ class Outer extends React.Component {
232
+ UNSAFE_componentWillMount() {
233
+ this.setState({x: 1});
234
+ }
235
+ render() {
236
+ return <Inner updateParent={this.updateParent}>{this.state.x}</Inner>;
237
+ }
238
+ updateParent = () => {
239
+ this.setState({x: 3});
240
+ };
241
+ }
242
+ class Inner extends React.Component {
243
+ UNSAFE_componentWillMount() {
244
+ this.setState({x: 2});
245
+ this.props.updateParent();
246
+ }
247
+ render() {
248
+ return <div>{this.props.children + '-' + this.state.x}</div>;
249
+ }
250
+ }
251
+ expect(() => {
252
+ // Shouldn't be 1-3.
253
+ expect(ReactDOMServer.renderToStaticMarkup(<Outer />)).toBe(
254
+ '<div>1-2</div>',
255
+ );
256
+ }).toWarnDev(
257
+ 'Warning: setState(...): Can only update a mounting component. This ' +
258
+ 'usually means you called setState() outside componentWillMount() on ' +
259
+ 'the server. This is a no-op.\n\n' +
260
+ 'Please check the code for the Outer component.',
261
+ );
262
+ });
263
});
packages/react-dom/src/server/ReactPartialRenderer.js
+24
-32
@@ -381,36 +381,26 @@ function resolve(
381
child: mixed,
382
context: Object,
383
|} {
384
- let element: ReactElement;
385
-
386
- let Component;
387
- let publicContext;
388
- let inst, queue, replace;
389
- let updater;
390
-
391
- let initialState;
392
- let oldQueue, oldReplace;
393
- let nextState, dontMutate;
394
- let partial, partialState;
395
-
396
- let childContext;
397
- let childContextTypes, contextKey;
398
-
384
while (React.isValidElement(child)) {
385
// Safe because we just checked it's an element.
401
- element = ((child: any): ReactElement);
386
+ let element: ReactElement = (child: any);
387
+ let Component = element.type;
388
if (__DEV__) {
389
pushElementToDebugStack(element);
390
}
405
- Component = element.type;
391
if (typeof Component !== 'function') {
392
break;
393
}
409
- publicContext = processContext(Component, context);
394
+ processChild(element, Component);
395
+ }
396
+
397
+ // Extra closure so queue and replace can be captured properly
398
+ function processChild(element, Component) {
399
+ let publicContext = processContext(Component, context);
400
411
- queue = [];
412
- replace = false;
413
- updater = {
401
+ let queue = [];
402
+ let replace = false;
403
+ let updater = {
404
isMounted: function(publicInstance) {
405
return false;
406
},
@@ -433,6 +423,7 @@ function resolve(
423
},
424
};
425
426
+ let inst;
427
if (shouldConstruct(Component)) {
428
inst = new Component(element.props, publicContext, updater);
429
@@ -453,7 +444,7 @@ function resolve(
444
}
445
}
446
456
- partialState = Component.getDerivedStateFromProps.call(
447
+ let partialState = Component.getDerivedStateFromProps.call(
448
null,
449
element.props,
450
inst.state,
@@ -502,7 +493,7 @@ function resolve(
493
if (inst == null || inst.render == null) {
494
child = inst;
495
validateRenderResult(child, Component);
505
- continue;
496
+ return;
497
}
498
}
499
@@ -510,7 +501,7 @@ function resolve(
501
inst.context = publicContext;
502
inst.updater = updater;
503
513
- initialState = inst.state;
504
+ let initialState = inst.state;
505
if (initialState === undefined) {
506
inst.state = initialState = null;
507
}
@@ -558,19 +549,19 @@ function resolve(
549
inst.UNSAFE_componentWillMount();
550
}
551
if (queue.length) {
561
- oldQueue = queue;
562
- oldReplace = replace;
552
+ let oldQueue = queue;
553
+ let oldReplace = replace;
554
queue = null;
555
replace = false;
556
557
if (oldReplace && oldQueue.length === 1) {
558
inst.state = oldQueue[0];
559
} else {
569
- nextState = oldReplace ? oldQueue[0] : inst.state;
570
- dontMutate = true;
560
+ let nextState = oldReplace ? oldQueue[0] : inst.state;
561
+ let dontMutate = true;
562
for (let i = oldReplace ? 1 : 0; i < oldQueue.length; i++) {
572
- partial = oldQueue[i];
573
- partialState =
563
+ let partial = oldQueue[i];
564
+ let partialState =
565
typeof partial === 'function'
566
? partial.call(inst, nextState, element.props, publicContext)
567
: partial;
@@ -600,11 +591,12 @@ function resolve(
591
}
592
validateRenderResult(child, Component);
593
594
+ let childContext;
595
if (typeof inst.getChildContext === 'function') {
604
- childContextTypes = Component.childContextTypes;
596
+ let childContextTypes = Component.childContextTypes;
597
if (typeof childContextTypes === 'object') {
598
childContext = inst.getChildContext();
607
- for (contextKey in childContext) {
599
+ for (let contextKey in childContext) {
600
invariant(
601
contextKey in childContextTypes,
602
'%s.getChildContext(): key "%s" is not defined in childContextTypes.',