React Portal之所以叫Portal,因为做的就是和“传送门”一样的事情:render到一个组件里面去,实际改变的是网页上另一处的DOM结构。
实际上解决的痛点:一个组件的交互涉及到一个非父类组件 这时候要么用全局状态管理传数据 要么通过"传送门"直接推一个子组件过去
传送门 v16之前的实现:
基于API:
unstable_renderSubtreeIntoContainer
unmountComponentAtNode
import React from 'react';
import {unstable_renderSubtreeIntoContainer, unmountComponentAtNode}
from 'react-dom';
class Dialog extends React.Component {
render() {
return null;
}
componentDidMount() {
const doc = window.document;
this.node = doc.createElement('div');
doc.body.appendChild(this.node);
this.renderPortal(this.props);
}
componentDidUpdate() {
this.renderPortal(this.props);
}
componentWillUnmount() {
unmountComponentAtNode(this.node);
window.document.body.removeChild(this.node);
}
renderPortal(props) {
unstable_renderSubtreeIntoContainer(
this, //代表当前组件
<div class="dialog">
{props.children}
</div>, // 塞进传送门的JSX
this.node // 传送门另一端的DOM node
);
}
}
总结:
它什么都不给自己画,render返回一个null就够了;
它做得事情是通过调用renderPortal把要画的东西画在DOM树上另一个角落。
传送门 v16的实现:
在v16中,使用Portal创建Dialog组件简单多了,不需要牵扯到componentDidMount、componentDidUpdate,也不用调用API清理Portal,关键代码在render中,像下面这样就行。(脏活render()都干了)
import React from 'react';
import {createPortal} from 'react-dom';
class Dialog extends React.Component {
constructor() {
super(...arguments);
const doc = window.document;
this.node = doc.createElement('div');
doc.body.appendChild(this.node);
}
render() {
return createPortal(
<div class="dialog">
{this.props.children}
</div>, //塞进传送门的JSX
this.node //传送门的另一端DOM node
);
}
componentWillUnmount() {
window.document.body.removeChild(this.node);
}
}
3.1官方的v16传送门实现
// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
// Let's create a Modal component that is an abstraction around
// the portal API.
class Modal extends React.Component {
constructor(props) {
super(props);
// Create a div that we'll render the modal into. Because each
// Modal component has its own element, we can render multiple
// modal components into the modal container.
this.el = document.createElement('div');
}
componentDidMount() {
// Append the element into the DOM on mount. We'll render
// into the modal container element (see the HTML tab).
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
// Remove the element from the DOM when we unmount
modalRoot.removeChild(this.el);
}
render() {
// Use a portal to render the children into the element
return ReactDOM.createPortal(
// Any valid React child: JSX, strings, arrays, etc.
this.props.children,
// A DOM element
this.el,
);
}
}
- v16之前传送出去的组件不冒泡回来了(应该是在传送出去的那一端冒泡 有待考证) v16之后传送出去的组件事件会冒泡回来
// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
modalRoot.removeChild(this.el);
}
render() {
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {clicks: 0};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// This will fire when the button in Child is clicked,
// updating Parent's state, even though button
// is not direct descendant in the DOM.
this.setState(prevState => ({
clicks: prevState.clicks + 1
}));
}
render() {
return (
<div onClick={this.handleClick}>
<p>Number of clicks: {this.state.clicks}</p>
<p>
Open up the browser DevTools
to observe that the button
is not a child of the div
with the onClick handler.
</p>
<Modal>
<Child />
</Modal>
</div>
);
}
}
function Child() {
// The click event on this button will bubble up to parent,
// because there is no 'onClick' attribute defined
return (
<div className="modal">
<button>Click</button>
</div>
);
}
ReactDOM.render(<Parent />, appRoot);