React18新特性+用例

68 阅读4分钟

查看React17

createRoot

react18更新修改了根节点的渲染方式

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

自动批处理优化

在 v18 之前只在事件处理函数中实现了批处理,在 v18 中所有更新都将自动批处理,包括 promise链、setTimeout等异步代码以及原生事件处理函数

批处理: React将多个状态更新分组到一个重新渲染中以获得更好的性能。(将多次 setstate 事件合并)

渲染优先级

在react18中可以指定渲染优先级,对于低优先级的会被高优先级的渲染中断

新Hooks

useId

是一个新的钩子,用于在客户端和服务器上生成唯一的 ID,同时避免水合作用不匹配。它主要用于与需要唯一 ID 的辅助功能 API 集成的组件库。这解决了 React 17 及更低版本中已经存在的问题,但在 React 18 中更为重要,因为新的流式服务器渲染器如何无序地交付 HTML。

当一个组件,同时会被服务端和客户端渲染时,我们就可以使用 useId 来创建当前组件的唯一身份。

import './App.css';
import { useDeferredValue, useId, useInsertionEffect, useRef, useState } from 'react';
​
function App() {
  let ref = useRef()
  const [state, setState] = useState(0)
​
  let id = useId() // 每次渲染id的值都是固定的,格式:r3:、:r4:
  let id1 = useId()
  let id2 = useId()
  let fn = () => {
    setState(state + 1)
  }
​
​
  return (
    <div className="App">
​
      <div ref={ref}>
        {id}
        <br></br>
        {id}
        <br></br>
        {id2}
        <br ></br>
        {state}
      </div>
      <button onClick={fn}>+1</button>
    </div>
  );
}
​
export default App;

startTransition

允许您将某些状态更新标记为不紧急。默认情况下,其他状态更新被视为紧急更新。React 将允许紧急状态更新(例如,更新文本输入)中断非紧急状态更新(例如,呈现搜索结果列表)。

import './App.css';
import { useDeferredValue, useInsertionEffect, useRef, useState, useTransition } from 'react';
​
function  App() {
  let ref = useRef()
  const [state, setState] = useState(0)
  const [state1, setState1] = useState(1)
  console.log("state",state)
  console.log("state1",state1)
​
  for(let i = 0;i<999;i++){ //  模拟线程繁忙
    console.log(i)
  }
​
​
  let [isPending,startTransition] = useTransition()
  let fn = () => {
    setState(state + 1)
    // setState1(state1 + 1)    假设:同时对state和state1的更新会导致卡顿,如果将transition注释,同时解开setState1(state1 + 1)。那么就满足了假设条件。因为react会将setState(state + 1)和setState1(state1 + 1)合并,函数重新执行一次。满足同时对state和state1的更新。所以我们可以将setState1(state1 + 1)放在transition中执行,但是要注意的是造成的函数执行是可中断的、延迟的
​
​
    startTransition(()=>{   // 在transition里修改state造成的更新和useDeferredValue是一样的可中断的、延迟的。
                        // 证明:快速点击+1按钮会发现state的值会大于state1
      setState1(state1 + 1)
    })
   
  }
​
  const handleChange = (e) => {
    // setState(e.target.value);
  };
  return (
    <div className="App">
      <input value={state} onChange={handleChange} />
      <div ref={ref}>
​
        {state}
        <br></br>
        {state1}
      </div>
      <button onClick={fn}>+1</button>
    </div>
  );
}
​
export default App;

useDeferredValue

允许您延迟重新渲染树的非紧急部分。它类似于去抖动,但与之相比有一些优点。没有固定的时间延迟,因此 React 将在第一个渲染反映在屏幕上后立即尝试延迟渲染。延迟的呈现是可中断的,不会阻止用户输入。

import './App.css';
import { useDeferredValue, useInsertionEffect, useRef, useState } from 'react';
​
function  App() {
  let ref = useRef()
  const [state, setState] = useState(0)
  let s = useDeferredValue(state)
​
  console.log("useDeferredValue",s)
  for(let i = 0;i<999;i++){
    console.log(i)
  }
​
  let fn = () => {
    setState(state + 1)
  }
  useInsertionEffect(() => {
    console.log(ref)
  })
  const handleChange = (e) => {
    setState(e.target.value);
  };
  return (
    <div className="App">
      <input value={state} onChange={handleChange} />
      <div ref={ref}>
        <div>{s}</div>
        
        <br></br>
        {state}
      </div>
      <button onClick={fn}>+1</button>
    </div>
  );
}
​
export default App;
// 连续多次点击按钮,查看s的值,可以发现s的值并发可中断的。所以我们可以将一些渲染等级较低的数据放在useDeferredValue中。

结论

useDeferredValue和setState一样设置新值,会导致函数组件重新执行渲染,但是useDeferredValue导致的渲染是可中断(不可恢复)的(高优先级需要渲染)、延迟的(空闲时渲染)

useSyncExternalStore

它允许外部存储通过强制对存储的更新是同步的来支持并发读取。它消除了在实现对外部数据源的订阅时的需要,并且建议用于与 React 外部状态集成的任何库。

import {
  createRef,
  useState,
  startTransition,
  useSyncExternalStore
} from "react";
​
function getItems(length) {
  const items = new Array(length);
  items.fill("");
  return items;
}
​
function createStore() {
  const ref = createRef();
  ref.current = { x: 0 };
  window.addEventListener("mousemove", (e) => {
    ref.current.x = e.clientX;
  });
  return ref.current;
}
​
function useX(store) {
  // return store.x;
  return useSyncExternalStore(
    () => { console.log("更新完成",store.x) }, // 数据一致被确定时执行该函数,在这里store.x是固定的值
    () => {
      console.log(store.x) // 数据不一致未被确定时执行该函数,在这里store.x是变化的值
      return store.x // 
    }
  );
}
​
const myStore = createStore();
​
function MouseX() {
  const x = useX(myStore);
  const now = performance.now();
  while (performance.now() - now < 10) { }
  return <li>{x}</li>;
}
​
export default function App() {
  const [count, setCount] = useState(0);
​
  function onClick() {
    // setCount(count + 1);
    startTransition(() => {
      setCount(count + 1);
    });
  }
​
  return (
    <div className="App">
      <button onClick={onClick}>Update X: {count}</button>
      <ul>
        {getItems(30).map((item, index) => (
          <MouseX key={index} />
        ))}
      </ul>
    </div>
  );
}
​

useInsertionEffect

是一个新的钩子,它允许CSS-in-JS库解决在渲染中注入样式的性能问题。除非你已经构建了一个CSS-in-JS库,否则我们不希望你使用它。此挂接将在 DOM 发生突变后运行,但在布局效果读取新布局之前运行。这解决了 React 17 及更低版本中已经存在的问题,但在 React 18 中更为重要,因为 React 在并发渲染期间屈服于浏览器,使其有机会重新计算布局。

使用方式和useEffect一样,只不过只是时机不一样,并且无法获取最新ref,一般在此(DOM改变前)修改一些样式

useInsertionEffect执行时机.png