在React中使用新的门户功能
React v16 引入了一个名为 portals 的新功能。 该文件指出:
门户提供了一种一流的方式来将子级呈现到存在于父组件的 DOM 层次结构之外的 DOM 节点中。
通常,函数式或类组件会渲染一棵 React 元素树(通常从 JSX 生成)。 React 元素定义了父组件的 DOM 的外观。
在 v16 之前,只允许渲染少数子类型:
null
或false
(表示不渲染)。- JSX。
- 反应元素。
function Example(props) { return null; } function Example(props) { return false; } function Example(props) { return <p>Some JSX</p>; } function Example(props) { return React.createElement( 'p', null, 'Hand coded' ); }
在 v16 中,更多的子类型变成了可渲染的:
- 数字(包括
Infinity
和NaN
)。 - 字符串。
- 反应门户。
- 一组可渲染的孩子。
function Example(props) { return 42; // Becomes a text node. } function Example(props) { return 'The meaning of life.'; // Becomes a text node. } function Example(props) { return ReactDOM.createPortal( // Any valid React child type [ 'A string', <p>Some JSX</p>, 'etc' ], props.someDomNode ); }
React 门户是通过调用 ReactDOM.createPortal 创建的。 第一个参数应该是一个可渲染的孩子。 第二个参数应该是对渲染可渲染子节点的 DOM 节点的引用。 ReactDOM.createPortal
返回一个本质上类似于 React.createElement 返回的对象。
请注意,createPortal
位于 ReactDOM
命名空间中,而不是像 createElement
那样的 React
命名空间。
一些细心的读者可能已经注意到,ReactDOM.createPortal
签名与 ReactDOM.render
是一样的,这样很容易记住。 但是,与 ReactDOM.render
不同,ReactDOM.createPortal
返回一个可渲染的子节点,该子节点在 协调 过程中使用。
何时使用
当父组件声明了 overflow: hidden
或具有影响 堆栈上下文 的属性并且您需要在视觉上“突破”其容器时,React 门户非常有用。 一些示例包括对话框、全局消息通知、悬停卡片和工具提示。
事件通过门户冒泡
React 文档很好地解释了这一点。
尽管门户可以位于 DOM 树中的任何位置,但它在其他方面表现得像一个普通的 React 子级。 无论孩子是否是门户,上下文等功能的工作方式完全相同,因为门户仍然存在于 React 树中,无论其在 DOM 树中的位置如何。
这包括事件冒泡。 从门户内部触发的事件将传播到包含 React 树中的祖先,即使这些元素不是 DOM 树中的祖先。
这使得侦听对话框、悬停卡片等中的事件就像它们在与父组件相同的 DOM 树中呈现一样简单。
例子
在下面的示例中,我们将利用 React 门户及其事件冒泡功能。
标记从以下内容开始。
<div class="PageHolder"> </div> <div class="DialogHolder is-empty"> <div class="Backdrop"></div> </div> <div class="MessageHolder"> </div>
.PageHolder
div
是我们应用程序的主要部分。 .DialogHolder
div
将是呈现任何生成的对话框的位置。 .MessageHolder
div
将是呈现任何生成的消息的位置。
因为我们希望所有对话框都显示在应用程序的主要部分上方,所以 .DialogHolder
div
声明了 z-index: 1
。 这将创建一个独立于 .PageHolder
的堆叠上下文的新堆叠上下文。
因为我们希望所有消息都显示在任何对话框上方,所以 .MessageHolder
div
声明了 z-index: 1
。 这将为 .DialogHolder
的堆叠上下文创建同级堆叠上下文。 尽管同级堆栈上下文的 z-index
具有相同的值,但由于 .MessageHolder
在 DOM 树中位于 .DialogHolder
之后,因此仍然会呈现我们想要的样子。
以下 CSS 总结了建立所需堆叠上下文的必要规则。
.PageHolder { /* Just use stacking context of parent element. */ /* A z-index: 1 would still work here. */ } .DialogHolder { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1; } .MessageHolder { position: fixed; top: 0; left: 0; width: 100%; z-index: 1; }
该示例将包含一个 Page
组件,该组件将被渲染为 .PageHolder
。
class Page extends React.Component { /* ... */ } ReactDOM.render( <Page/>, document.querySelector('.PageHolder') )
因为我们的 Page
组件将分别将对话框和消息渲染到 .DialogHolder
和 .MessageHolder
中,因此它需要引用这些持有者 div
渲染时间。 我们有几种选择。
我们可以在渲染 Page
组件之前解析对这些持有者 div
的引用,并将它们作为属性传递给 Page
组件。
let dialogHolder = document.querySelector('.DialogHolder'); let messageHolder = document.querySelector('.MessageHolder'); ReactDOM.render( <Page dialogHolder={dialogHolder} messageHolder={messageHolder}/>, document.querySelector('.PageHolder') );
我们可以将选择器作为属性传递给 Page
组件,然后解析 componentWillMount
中的引用以进行初始渲染,如果选择器更改,则在 componentWillReceiveProps
中重新解析。
class Page extends React.Component { constructor(props) { super(props); let { dialogHolder = '.DialogHolder', messageHolder = '.MessageHolder' } = props this.state = { dialogHolder, messageHolder, } } componentWillMount() { let state = this.state, dialogHolder = state.dialogHolder, messageHolder = state.messageHolder this._resolvePortalRoots(dialogHolder, messageHolder); } componentWillReceiveProps(nextProps) { let props = this.props, dialogHolder = nextProps.dialogHolder, messageHolder = nextProps.messageHolder if (props.dialogHolder !== dialogHolder || props.messageHolder !== messageHolder ) { this._resolvePortalRoots(dialogHolder, messageHolder); } } _resolvePortalRoots(dialogHolder, messageHolder) { if (typeof dialogHolder === 'string') { dialogHolder = document.querySelector(dialogHolder) } if (typeof messageHolder === 'string') { messageHolder = document.querySelector(messageHolder) } this.setState({ dialogHolder, messageHolder, }) } }
现在我们已经确保我们将拥有门户的 DOM 引用,我们可以使用对话框和消息来渲染页面组件。
就像 React 元素一样,React 门户是基于组件属性和状态呈现的。 对于此示例,我们将有两个按钮。 一种将创建对话门户,以便在单击时在对话框持有者中呈现,另一种将创建要在消息持有者中呈现的消息门户。 我们将在组件的状态中保留对这些门户的引用,这将在渲染方法中使用。
class Page extends React.Component { // ... constructor(props) { super(props); let { dialogHolder = '.DialogHolder', messageHolder = '.MessageHolder' } = props this.state = { dialogHolder, dialogs: [], messageHolder, messages: [], } } render() { let state = this.state, dialogs = state.dialogs, messages = state.messages return ( <div className="Page"> <button onClick={evt => this.addNewDialog()}> Add Dialog </button> <button onClick={evt => this.addNewMessage()}> Add Message </button> {dialogs} {messages} </div> ) } addNewDialog() { let dialog = ReactDOM.createPortal(( <div className="Dialog"> ... </div> ), this.state.dialogHolder ) this.setState({ dialogs: this.state.dialogs.concat(dialog), }) } addNewMessage() { let message = ReactDOM.createPortal(( <div className="Message"> ... </div> ), this.state.messageHolder ) this.setState({ messages: this.state.messages.concat(message), }) } // ... }
为了演示事件会从 React 门户组件冒泡到父组件,让我们在 .Page
div
上添加一个点击处理程序。
class Page extends React.Component { // ... render() { let state = this.state, dialogs = state.dialogs, messages = state.messages return ( <div className="Page" onClick={evt => this.onPageClick(evt)}> ... </div> ) } onPageClick(evt) { console.log(`${evt.target.className} was clicked!`); } // ... }
单击对话框或消息时,将调用 onPageClick
事件处理程序(只要另一个处理程序没有停止传播)。
请参阅上述演示的 工作示例 。
👉 当您遇到 overflow: hidden
或堆叠上下文问题时,请使用 React 门户!