diff --git a/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js new file mode 100644 index 0000000000000..f9b212012b101 --- /dev/null +++ b/packages/react-dom/src/__tests__/ReactDOMServerLifecycles-test.js @@ -0,0 +1,150 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @emails react-core + */ + +'use strict'; + +const ReactDOMServerIntegrationUtils = require('./utils/ReactDOMServerIntegrationTestUtils'); + +let React; +let ReactDOMServer; + +function initModules() { + // Reset warning cache. + jest.resetModuleRegistry(); + React = require('react'); + ReactDOMServer = require('react-dom/server'); + + // Make them available to the helpers. + return { + ReactDOMServer, + }; +} + +const {resetModules} = ReactDOMServerIntegrationUtils(initModules); + +describe('ReactDOMServerLifecycles', () => { + beforeEach(() => { + resetModules(); + }); + + it('should invoke the correct lifecycle hooks', () => { + const log = []; + + class Outer extends React.Component { + unsafe_componentWillMount() { + log.push('outer componentWillMount'); + } + render() { + log.push('outer render'); + return ; + } + } + + class Inner extends React.Component { + unsafe_componentWillMount() { + log.push('inner componentWillMount'); + } + render() { + log.push('inner render'); + return null; + } + } + + ReactDOMServer.renderToString(); + expect(log).toEqual([ + 'outer componentWillMount', + 'outer render', + 'inner componentWillMount', + 'inner render', + ]); + }); + + it('should warn about deprecated lifecycle hooks', () => { + class Component extends React.Component { + componentWillMount() {} + render() { + return null; + } + } + + expect(() => ReactDOMServer.renderToString()).toWarnDev( + 'Warning: Component: componentWillMount() is deprecated and will be removed ' + + 'in the next major version. Please use unsafe_componentWillMount() instead.', + ); + }); + + it('should update instance.state with value returned from getDerivedStateFromProps', () => { + class Grandparent extends React.Component { + state = { + foo: 'foo', + }; + render() { + return ( +
+ {`Grandparent: ${this.state.foo}`} + +
+ ); + } + } + + class Parent extends React.Component { + state = { + bar: 'bar', + baz: 'baz', + }; + static getDerivedStateFromProps(props, prevState) { + return { + bar: `not ${prevState.bar}`, + }; + } + render() { + return ( +
+ {`Parent: ${this.state.bar}, ${this.state.baz}`} + ; +
+ ); + } + } + + class Child extends React.Component { + static getDerivedStateFromProps() { + return { + qux: 'qux', + }; + } + render() { + return `Child: ${this.state.qux}`; + } + } + + const markup = ReactDOMServer.renderToString(); + expect(markup).toContain('Grandparent: foo'); + expect(markup).toContain('Parent: not bar, baz'); + expect(markup).toContain('Child: qux'); + }); + + it('should warn if getDerivedStateFromProps returns undefined', () => { + class Component extends React.Component { + static getDerivedStateFromProps() {} + render() { + return null; + } + } + + expect(() => ReactDOMServer.renderToString()).toWarnDev( + 'Component.getDerivedStateFromProps(): A valid state object (or null) must ' + + 'be returned. You may have returned undefined.', + ); + + // De-duped + ReactDOMServer.renderToString(); + }); +}); diff --git a/packages/react-dom/src/server/ReactPartialRenderer.js b/packages/react-dom/src/server/ReactPartialRenderer.js index 6ca0ff4ffb116..144c59932f0ca 100644 --- a/packages/react-dom/src/server/ReactPartialRenderer.js +++ b/packages/react-dom/src/server/ReactPartialRenderer.js @@ -123,6 +123,7 @@ let didWarnDefaultTextareaValue = false; let didWarnInvalidOptionChildren = false; const didWarnAboutNoopUpdateForComponent = {}; const didWarnAboutBadClass = {}; +const didWarnAboutUndefinedDerivedState = {}; const valuePropNames = ['value', 'defaultValue']; const newlineEatingTags = { listing: true, @@ -421,6 +422,33 @@ function resolve( if (shouldConstruct(Component)) { inst = new Component(element.props, publicContext, updater); + + if (typeof Component.getDerivedStateFromProps === 'function') { + partialState = Component.getDerivedStateFromProps( + element.props, + inst.state, + ); + + if (__DEV__) { + if (partialState === undefined) { + const componentName = getComponentName(Component) || 'Unknown'; + + if (!didWarnAboutUndefinedDerivedState[componentName]) { + warning( + false, + '%s.getDerivedStateFromProps(): A valid state object (or null) must be returned. ' + + 'You may have returned undefined.', + componentName, + ); + didWarnAboutUndefinedDerivedState[componentName] = true; + } + } + } + + if (partialState != null) { + inst.state = Object.assign({}, inst.state, partialState); + } + } } else { if (__DEV__) { if (