你不知道的React系列(四十三)ReactDOM

218 阅读5分钟

概览

在应用顶层使用的 DOM(DOM-specific)方法

用于 React 模型以外的地方

浏览器支持

React 支持所有的现代浏览器,包括 IE9 及以上版本,但是需要为旧版浏览器比如 IE9 和 IE10 引入相关的 polyfills 依赖

API

  • render()(已废弃)

    ReactDOM.render(element, container[, callback])

    • container 里渲染 element 元素或者组件,并返回对该组件的引用(函数组件和React 元素返回 null)

      推荐为根元素添加 callback ref

    • 回调函数在组件被渲染或更新之后被执行

    • ReactDOM.render() 首次调用时,容器节点里的所有 DOM 元素都会被替换

      后续的调用则会使用diff进行高效的更新

    • ReactDOM.render() 不会修改容器节点

      • 只会修改容器的子节点
      • 可以在不覆盖现有子节点的情况下,将组件插入已有的 DOM 节点中
  • hydrate()(已废弃)

    ReactDOM.hydrate(element, container[, callback])

  • unmountComponentAtNode()(已废弃)

    ReactDOM.unmountComponentAtNode(container)

    • 从 DOM 中卸载组件,会将其事件处理器(event handlers)和 state 一并清除

    • 组件被移除将会返回 true,如果没有组件可被移除将会返回 false

  • findDOMNode()(已废弃)

    • 严格模式下该方法已弃用

    • 返回浏览器中相应的原生 DOM 元素

    • 当组件渲染的内容为 null 或 false 时,findDOMNode 也会返回 null

    • 当组件渲染的是字符串时,findDOMNode 返回的是字符串对应的 DOM 节点

    • 返回有多个子节点的 fragment,findDOMNode 会返回第一个非空子节点对应的 DOM 节点。

    • findDOMNode 不能用于函数组件

  • flushSync

    flushSync(callback)
    

    强制同步更新回调函数内容

    • 影响性能尽量不用

    • Suspense 可能展示 fallback

    • 可以使用副作用返回之前同步应用它们所包含的任何更新

    • 当需要刷新 callback 内部的更新时,flushSync 可能更新 callback 外部更新

      例如,按钮点击以后需要立刻更新 DOM onbeforeprint

      import { useState, useEffect } from 'react';
      import { flushSync } from 'react-dom';
      
      export default function PrintApp() {
        const [isPrinting, setIsPrinting] = useState(false);
      
        useEffect(() => {
          function handleBeforePrint() {
            flushSync(() => {
              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>
          </>
        );
      }
      
  • createElement()(已过时)

    创建 React 元素

    React.createElement(
      type,
      [props],
      [...children]
    )
    

    type:标签名、组件名、React fragment

    props:props对象

    children:DOM结构、组件名、数组

  • cloneElement()(已过时)

    克隆element元素

    config定义新props,key,ref,如果没有定义使用原始元素props,key,ref

    React.cloneElement(
      element,
      [config],
      [...children]
    )
    <element.type {...element.props} {...props}>{children}</element.type>
    
  • isValidElement()(已过时)

    验证对象是否为 React 元素

    React.isValidElement(object)
    
  • React.Children 相关函数(已过时)

    处理 this.props.children

    • React.Children.map(children, function[(thisArg)])
    • 如果 children 是一个数组,它将被遍历并为数组中的每个子节点调用该函数。
    • 如果子节点为 null 或是 undefined,则此方法将返回 null 或是 undefined,而不会返回数组
    • 如果 children 是一个 Fragment 对象,它将被视为单一子节点的情况处理,而不会被遍历
    • React.Children.forEach(children, function[(thisArg)])
    • React.Children.count(children)
    • React.Children.only(children)
    • 验证 children 是否只有一个子节点(一个 React 元素),如果有则返回它,否则此方法会抛出错误
    • React.Children.only() 不接受 [React.Children.map()](https://zh-hans.reactjs.org/docs/react-api.html#reactchildrenmap) 的返回值,因为它是一个数组而并不是 React 元素
    • React.Children.toArray(children)
    • 将 children 这个复杂的数据结构以数组的方式扁平展开并返回,并为每个子节点分配一个 key
    • 向下传递 this.props.children 之前对内容重新排序或获取子集
  • ReactDOMClient

    import * as ReactDOM from 'react-dom/client';
    
    • createRoot()

      const root = createRoot(domNode, options?)     
      
      • 创建root节点,管理 DOM

      • 不会修改 domNode 节点

      • 客户端

      • 想要渲染一个不是子组件时使用 createPortal

      • options

        • onRecoverableError

          React从错误恢复时执行

        • identifierPrefix

          • id前缀,React.useId,同一页面使用多个根节点避免冲突

          • 服务端必须是相同前缀

      • root.render(reactNode)

        reactNode 通常是一段 JSX 也可以是使用 createElement() 创建的

      • 第一次运行会清除已有的 HTML

      • 如果是服务端渲染或者构建时 HTML,使用 hydrateRoot

      • 同一个根节点多次运行 render 时会按照 React 相关更新规则

      • root.unmount();

        • 销毁已经渲染的内容

        • 第三方删除节点的时候使用

      • 只会执行一次

      • 全部使用 React 渲染

      • 部分使用 React 渲染

      • 更新根组件

        多次调用 Render 函数,组件结构没有变化 state 会保留

      问答

      • 创建了根组件,但是没有内容显示

        忘记了 root.render()

      • Target container is not a DOM element

        • domNode 不是 DOM 节点
        • ID 不匹配
        • script 标签没有渲染任何节点
        • createRoot(domNode) 写成了 createRoot()
      • Functions are not valid as a React child

      // 🚩 Wrong: App is a function, not a Component.
      root.render(App);
      
      // ✅ Correct: <App /> is a component.
      root.render(<App />);
      
      • 服务端渲染的 HTML 重新创建

        使用 hydrateRoot

    • hydrateRoot()

      const root = hydrateRoot(domNod, reactNode, options?)
      

      react-dom/server 生成 HTML 节点,使用 hydrateRoot 进行展示

      domNode 服务器上作为根节点渲染 renderToPipeableStream(<App />)

      reactNode 一段 JSX 使用服务器渲染

      • options

        • onRecoverableError

          React从错误恢复时执行

        • identifierPrefix

          • id前缀,React.useId

          • 服务端必须是相同前缀

      一定解决不匹配的错误提示

注意事项

hydrateRoot() 期望渲染内容和服务端渲染的内容一样

开发模式如果客户端和服务端代码不一致会提醒

只有一个 hydrateRoot

如果是客户端渲染使用 createRoot()

root.render()

root.unmount()

HTML 是由 react-dom/server 生成的需要在客户端使用 hydrate

Hydrating 整个文档

function App() {
  return (
    <html>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <link rel="stylesheet" href="/styles.css"></link>
        <title>My app</title>
      </head>
      <body>
        <Router />
      </body>
    </html>
  );
}
import { hydrateRoot } from 'react-dom/client';
import App from './App.js';

hydrateRoot(document, <App />);

减少 hydration 出现不可避免的错误

timestamp 时间戳,忽略提示

export default function App() {
  return (
    <h1 suppressHydrationWarning={true}>
      Current Date: {new Date().toLocaleDateString()}
    </h1>
  );
}

隐藏客户端和服务端不同的内容

客户端渲染和服务端不同的内容,使用都可以通过的 render

速度慢需要渲染两次

import { useState, useEffect } from "react";

export default function App() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <h1>
      {isClient ? 'Is Client' : 'Is Server'}
    </h1>
  );
}

更新已经 hydrated 组件

import { hydrateRoot } from 'react-dom/client';
import './styles.css';
import App from './App.js';

const root = hydrateRoot(
  document.getElementById('root'),
  <App counter={0} />
);

let i = 0;
setInterval(() => {
  root.render(<App counter={i} />);
  i++;
}, 1000);