/** * Copyright (c) Facebook, Inc. and its affiliates. * * 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 React = require('react'); const PropTypes = require('prop-types'); const ReactDOM = require('react-dom'); const ReactTestUtils = require('react-dom/test-utils'); const renderSubtreeIntoContainer = require('react-dom') .unstable_renderSubtreeIntoContainer; const ReactFeatureFlags = require('shared/ReactFeatureFlags'); describe('renderSubtreeIntoContainer', () => { it('should pass context when rendering subtree elsewhere', () => { const portal = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, }; render() { return
{this.context.foo}
; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, }; getChildContext() { return { foo: 'bar', }; } render() { return null; } componentDidMount() { if (ReactFeatureFlags.warnUnstableRenderSubtreeIntoContainer) { expect( function() { renderSubtreeIntoContainer(this, , portal); }.bind(this), ).toWarnDev( 'ReactDOM.unstable_renderSubtreeIntoContainer() is deprecated and ' + 'will be removed in a future major release. Consider using React Portals instead.', ); } else { renderSubtreeIntoContainer(this, , portal); } } } ReactTestUtils.renderIntoDocument(); expect(portal.firstChild.innerHTML).toBe('bar'); }); it('should throw if parentComponent is invalid', () => { const portal = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, }; render() { return
{this.context.foo}
; } } // ESLint is confused here and thinks Parent is unused, presumably because // it is only used inside of the class body? // eslint-disable-next-line no-unused-vars class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, }; getChildContext() { return { foo: 'bar', }; } render() { return null; } componentDidMount() { expect(function() { renderSubtreeIntoContainer(, , portal); }).toThrowError('parentComponentmust be a valid React Component'); } } }); it('should update context if it changes due to setState', () => { const container = document.createElement('div'); document.body.appendChild(container); const portal = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; render() { return
{this.context.foo + '-' + this.context.getFoo()}
; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; state = { bar: 'initial', }; getChildContext() { return { foo: this.state.bar, getFoo: () => this.state.bar, }; } render() { return null; } componentDidMount() { renderSubtreeIntoContainer(this, , portal); } componentDidUpdate() { renderSubtreeIntoContainer(this, , portal); } } const instance = ReactDOM.render(, container); expect(portal.firstChild.innerHTML).toBe('initial-initial'); instance.setState({bar: 'changed'}); expect(portal.firstChild.innerHTML).toBe('changed-changed'); }); it('should update context if it changes due to re-render', () => { const container = document.createElement('div'); document.body.appendChild(container); const portal = document.createElement('div'); class Component extends React.Component { static contextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; render() { return
{this.context.foo + '-' + this.context.getFoo()}
; } } class Parent extends React.Component { static childContextTypes = { foo: PropTypes.string.isRequired, getFoo: PropTypes.func.isRequired, }; getChildContext() { return { foo: this.props.bar, getFoo: () => this.props.bar, }; } render() { return null; } componentDidMount() { renderSubtreeIntoContainer(this, , portal); } componentDidUpdate() { renderSubtreeIntoContainer(this, , portal); } } ReactDOM.render(, container); expect(portal.firstChild.innerHTML).toBe('initial-initial'); ReactDOM.render(, container); expect(portal.firstChild.innerHTML).toBe('changed-changed'); }); it('should render portal with non-context-provider parent', () => { const container = document.createElement('div'); document.body.appendChild(container); const portal = document.createElement('div'); class Parent extends React.Component { render() { return null; } componentDidMount() { renderSubtreeIntoContainer(this,
hello
, portal); } } ReactDOM.render(, container); expect(portal.firstChild.innerHTML).toBe('hello'); }); it('should get context through non-context-provider parent', () => { const container = document.createElement('div'); document.body.appendChild(container); const portal = document.createElement('div'); class Parent extends React.Component { render() { return ; } getChildContext() { return {value: this.props.value}; } static childContextTypes = { value: PropTypes.string.isRequired, }; } class Middle extends React.Component { render() { return null; } componentDidMount() { renderSubtreeIntoContainer(this, , portal); } } class Child extends React.Component { static contextTypes = { value: PropTypes.string.isRequired, }; render() { return
{this.context.value}
; } } ReactDOM.render(, container); expect(portal.textContent).toBe('foo'); }); it('should get context through middle non-context-provider layer', () => { const container = document.createElement('div'); document.body.appendChild(container); const portal1 = document.createElement('div'); const portal2 = document.createElement('div'); class Parent extends React.Component { render() { return null; } getChildContext() { return {value: this.props.value}; } componentDidMount() { renderSubtreeIntoContainer(this, , portal1); } static childContextTypes = { value: PropTypes.string.isRequired, }; } class Middle extends React.Component { render() { return null; } componentDidMount() { renderSubtreeIntoContainer(this, , portal2); } } class Child extends React.Component { static contextTypes = { value: PropTypes.string.isRequired, }; render() { return
{this.context.value}
; } } ReactDOM.render(, container); expect(portal2.textContent).toBe('foo'); }); it('fails gracefully when mixing React 15 and 16', () => { class C extends React.Component { render() { return
; } } const c = ReactDOM.render(, document.createElement('div')); // React 15 calls this: // https://github.com/facebook/react/blob/77b71fc3c4/src/renderers/dom/client/ReactMount.js#L478-L479 expect(() => { c._reactInternalInstance._processChildContext({}); }).toThrow( __DEV__ ? '_processChildContext is not available in React 16+. This likely ' + 'means you have multiple copies of React and are attempting to nest ' + 'a React 15 tree inside a React 16 tree using ' + "unstable_renderSubtreeIntoContainer, which isn't supported. Try to " + 'make sure you have only one copy of React (and ideally, switch to ' + 'ReactDOM.createPortal).' : "Cannot read property '_processChildContext' of undefined", ); }); });