新版React官方文档解读(十)- react-dom API 之 createPortal 和 flushSync

大家好呀,我是小肚肚肚肚肚哦!这是我的React官网解读系列!

React 官网 beta 版本已经变为了正式版,但仍旧没有中文版。对于国内不少开发者来说增加了不少麻烦。我这里以前端开发的角度归纳总结一下,把其中大家重点使用的部分介绍给大家。

官网地址:React

本期来讲解一下 react-dom 库内置的 API 们。


createPortal

createPortal 用于手动渲染动态组件。

接受参数

  • children: 第一个参数是一个可以被 render 的 React DOM.
  • domNode: 第二个参数是一个真实的 DOM 节点,用于当做渲染的父容器.
  • 可选项 key: 第三个参数可选,作为动态组件的 key.

返回值

createPortal返回一个 React node. 在渲染时,React 会将指定的渲染组件放置在指定的 DOM 容器(domNode)内。

注意事项

  • 使用 createPortal 是会事件冒泡的. 举个例子,如果你在 portal 内部点击鼠标, 这个 portal 又被 <div onClick> 包裹, 则 onClick 事件也会被触发.

使用案例

  1. 往别的 div 里渲染一个组件
jsx 复制代码
export default function MyComponent() {
  return (
    <div style={{ border: '2px solid black' }}>
      <p>This child is placed in the parent div.</p>
      {createPortal(
        <p>This child is placed in the document body.</p>,
        document.body
      )}
    </div>
  );
}

看看界面,确实渲染在 body 下,原来的位置空空如也:

  1. 实现一个弹窗

如果你写过 UI 库,上面的 DOM 结构你可能会比较熟悉,这就是弹出层组件常用的插入方式。我们就用上面的原理,实现一个简单的弹窗,

js 复制代码
export default function PortalExample() {
  const [showModal, setShowModal] = useState(false);
  return (
    <>
      <button onClick={() => setShowModal(true)}>
        Show modal using a portal
      </button>
      {showModal && createPortal(
        <ModalContent onClose={() => setShowModal(false)} />,
        document.body
      )}
    </>
  );
}

如果不使用 createPortal,在 showModal 时,ModalContent 组件会追加在按钮下面,如果设置了绝对定位后,也只是针对上一级组件的偏移,而弹窗理应是个全局性的组件。我们看看运行效果:

  1. 服务端渲染时,往独立的 html 中渲染

服务端渲染时,会存在脱离 React 环境的页面 (比如 React root 只作为 web 项目的一部分时),使用这个方法就可以实现渲染:

js 复制代码
const sidebarContentEl = document.getElementById('sidebar-content');

...
<>
  <MainContent />
  {createPortal(
    <SidebarContent />,
    sidebarContentEl
  )}
</>
  1. 将只支持 js 引入方式的第三方库渲染为 React 组件

有的第三方库只提供了 js 引入方式,比如 地图组件 leaflet:

js 复制代码
// map-widget.js
import 'leaflet/dist/leaflet.css';
import * as L from 'leaflet';

export function createMapWidget(containerDomNode) {
  const map = L.map(containerDomNode);
  map.setView([0, 0], 0);
  L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '© OpenStreetMap'
  }).addTo(map);
  return map;
}

export function addPopupToMapWidget(map) {
  const popupDiv = document.createElement('div');
  L.popup()
    .setLatLng([0, 0])
    .setContent(popupDiv)
    .openOn(map);
  return popupDiv;
}

上面的两个函数都不是组件,我们不能在React组件中直接引用。

看下面的处理:

js 复制代码
import { useRef, useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { createMapWidget, addPopupToMapWidget } from './map-widget.js';

export default function Map() {
  const containerRef = useRef(null);
  const mapRef = useRef(null);
  const [popupContainer, setPopupContainer] = useState(null);

  useEffect(() => {
    if (mapRef.current === null) {
      const map = createMapWidget(containerRef.current);
      mapRef.current = map;
      const popupDiv = addPopupToMapWidget(map);
      setPopupContainer(popupDiv);
    }
  }, []);

  return (
    <div style={{ width: 250, height: 250 }} ref={containerRef}>
      {popupContainer !== null && createPortal(
        <p>Hello from React!</p>,
        popupContainer
      )}
    </div>
  );
}

上面的代码,使用 React 状态 popupContainer 接受了地图返回的容器 div,并包裹在组件 Map 中返回。如此,这个组件就可以被外部组件调用了。

flushSync

flushSync 用于强制刷新 React 所有已经挂起的渲染工作,并同步更新组件树。

在默认情况下,React 使用异步更新策略,这意味着在调用 setStateforceUpdate 等更新方法后,React 将在事件循环的当前迭代中批量更新组件,而不是立即更新(可以了解一下 fiber 的调度原理)。这种异步更新策略可以提高性能,但有时候我们需要立即更新组件,这就是 flushSync 的用途。

flushSync 方法可以强制 React 立即执行当前批次的所有更新,而不是等待事件循环的下一个迭代。

滥用会损害性能,最好不用。

接受参数

  • callback: 接受一个函数。会立即被调用,并刷新状态。同时也会刷新阻塞的更新任务、副作用等。

注意事项

  • flushSync 会显著影响性能.
  • flushSync 可能强制阻塞 Suspense 组件,以至于 fallback 无法显示.

使用案例

  1. 同一周期内渲染的问题

有时候想要在同一个渲染周期内获取到最新的 state 的值,比如下面的打印机的例子:

js 复制代码
import { useState, useEffect } from 'react';

export default function PrintApp() {
  const [isPrinting, setIsPrinting] = useState(false);

  useEffect(() => {
    function handleBeforePrint() {
      setIsPrinting(true);
    }

    function handleAfterPrint() {
      setIsPrinting(false);
    }

    window.addEventListener('beforeprint', handleBeforePrint);
    window.addEventListener('afterprint', handleAfterPrint);
    return () => {
      window.removeEventListener('beforeprint', handleBeforePrint);
      window.removeEventListener('afterprint', handleAfterPrint);
    }
  }, []);

  return (
    <>
      <h1>isPrinting: {isPrinting ? 'yes' : 'no'}</h1>
      <button onClick={() => window.print()}>
        Print
      </button>
    </>
  );
}

beforeprint 事件在调用打印页面时会被立即调用,但是你会发现,在界面上,isPrinting 却是 no。这是因为更新 state 的任务放入队列后,打印弹窗打开了,阻塞了任务。所以我们需要强制渲染:

js 复制代码
function handleAfterPrint() {
  flushSync(() => {
    setIsPrinting(false);
  }
}

这样,就会在打印弹窗打开的同时,同步渲染 UI 了。


到这里,常规的 API 就介绍完了,之后的系列文章就开始介绍客户端与服务端渲染的相关API了,敬请期待~~

相关推荐
LaughingZhu5 小时前
智能体经典范式构建:ReAct、Plan-and-Solve 与 Reflection
前端·react.js·前端框架
飞天狗6 小时前
长列表滚动卡顿怎么破:虚拟列表选型 + react-virtuoso 实战调优指南
react.js·性能优化
binbin_528 小时前
React 井字棋教程:用一个小游戏理解组件和状态
前端·react.js·前端框架
张元清9 小时前
React useObjectUrl Hook:预览文件与 Blob,不留内存泄漏(2026)
javascript·react.js
真的想不出名儿13 小时前
Md编辑器整合-react
前端·react.js·编辑器
牛仔只喝牛奶1 天前
多端跨端技术选型:小程序 + H5 + App 的两栈拆分落地
react native·react.js
wear工程师1 天前
setState 到底是不是异步的?这题从 React 16 问到 18,标准答案早就变了
react.js·面试
wanghowie1 天前
LangGraph4j 落地(二)— ReAct 多步工具与 trace 扩展
前端·react.js·前端框架
csj501 天前
前端基础之《React(11)—state声明式》
前端·react.js
张元清1 天前
React useLocalStorage Hook:SSR 安全的持久化状态(2026)
javascript·react.js