react18-学习

262 阅读10分钟

React v18 有哪些新功能

Concurrent Mode

React v18一个重要的特性就是Concurrent Mode,提升应用用户体验。

和旧版本相比Concurrent Mode 的核心是 渲染可以被打断

旧版本一次setState导致更新是不可以被打断的,一直到结果渲染的用户面前。

默认setState都是不可打断的,通过hook useTransition,包装的setState代表是低优先级的,可以被打断的更新。

什么是可被打断的渲染?

例子里面通过人为的长任务扩大render的时间,对于不可打断的例子,会发现一旦触发setState之后就会开始阻塞响应。

但是对于可以被打断的ReactV18版本,在长任务render的时候,是不会阻塞其他setState的响应的。

可被打断渲染的粒度

例子

通过扩大单个组件的render时间,可以发现input的输入出现了卡顿。所以可以被打断渲染的粒度是组件级别的。单个组件的render是不可被打断的。

低优先级一直被打断会发生什么

例子

一直在input上输入内容后setState,低优先级的任务会被一直打断,会发现一段时间后会强制执行低优先级的任务,阻塞相应。

高低优先级运行顺序

例子

本质上React还是单线程,只是通过不同的优先级调度去实现并发渲染。


新功能

automatic batching

React V18之前,只有在被react包装的事件里面,setState是异步更新的。对于promisesetTimeout、原生事件的绑定里面setState都是同步更新的。

自动批量更新可以减少很多render。

升级到V18后对于需要同步更新的情况,可以使用 flushSync

automatic batching可能的问题

class 组件,下面的写法会有问题,之前的版本是可以打印最新的state的,automatic batching后就不可以了。

setTime(() => {
    this.setState({ count: 1 });
    console.log(this.state.count);
}, 100);

需要改造成

setTime(() => {
    flushSync(() => this.setState({ count: 1 }));
    console.log(this.state.count);
}, 100);
Transitions

Transitions是ReactV18的新概念,用于区分 紧急更新非紧急更新

  • 紧急更新:主要是用户的一些交互,比如输入、点击等
  • 非紧急更新:从一个视图切换到另一个

eg: 根据用户的输入进行列表数据的过滤,对于用户的输入内容是属于紧急的更新,但是 过滤的列表结果 展示属于 非紧急的更新。不能让列表的渲染阻塞了用户的输入。

创建非紧急更新

默认所有的setState都属于紧急更新,通过hookuseTransitions和apistartTransitions包裹的函数内的setState都属于非紧急更新

import {startTransition} from 'react';

startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});
import {useTransitions} from 'react';
// ....
const Demo = () => {
    const [isPending, startTransition] = useTransitions();

    startTransition(() => {
      // Transition: Show the results
      setSearchQuery(input);
    });
    // ....
}

Transitions包裹的更新就是可被打断的更新。

Transitions 和 setTimeout的对比
import { startTransition } from 'react';


// Urgent: Show what was typed
setInputValue(input);

// Mark any state updates inside as transitions
startTransition(() => {
  // Transition: Show the results
  setSearchQuery(input);
});
// Show what you typed
setInputValue(input);

// Show the results
setTimeout(() => {
  setSearchQuery(input);
}, 0);

setTimout也可以做到延迟更新结果的目的。

对比起来Transitions的优势有

  1. Transitions包裹的函数是立即执行的,只是状态更新是延迟的,setTimout整体是延迟执行。
  2. 耗时长的render依然会阻塞setTimout的执行。

Questions about specifics of Concurrent scheduling

Suspense

ReactV18 可以通过Suspense去做数据请求。搭配一些第三方框架RelayNext.jsRemixHydrogen

不久的将来,React会完善Suspense的功能,不依赖框架就可以方便的处理数据请求。

当前我们都是在code-split的时候搭配 React.lazySuspense去进行代码加载。但是React对于Suspense的定位是可以处理任何异步的功能(eg: 加载数据、加载资源等)。

Suspense的设计类似于 throwcatch。被throw的错误都是被离得最近的catch捕获。只不过Suspense捕获的是promise

<Suspense fallback={<PageGlimmer />}>
  <RightColumn>
    <ProfileHeader />
  </RightColumn>
  <LeftColumn>
    <Suspense fallback={<LeftColumnGlimmer />}>
      <Comments />
      <Photos />
    </Suspense>
  </LeftColumn>
</Suspense>

ProfileHeader组件没有准备好的时候,会展示fallback PageGlimmer。但是CommentsPhotos组件没有准备好的时候,展示的是LeftColumnGlimmer不会影响RightColumn的展示。

基础用法-demo

注意:

  • Suspense只能捕获到组件的异步数据加载,对于useEffect和 event handler里面的trow promise是做不到捕获的。跟ErrorBoundary一样,只能捕获渲染过程中的

使用方式:

  1. 同时展示内容,子组件内部都没有Suspense,最外层的Suspense只有在所有所有子组件都准备好之后才会展示内容(跟Promise.all一样)。demo
<Suspense fallback={<Loading />}>
  <Biography />
  <Panel>
    <Albums />
  </Panel>
</Suspense>
  1. 嵌套加载,Biography加载完成后就会移除BigSpinner展示内容。demo(ps: 通过demo发现接口请求还是瀑布流,Biography结束后,Albums的接口才会开始请求)
<Suspense fallback={<BigSpinner />}>
  <Biography />
  <Suspense fallback={<AlbumsGlimmer />}>
    <Panel>
      <Albums />
    </Panel>
  </Suspense>
</Suspense>

Suspense目前的功能不搭配第三方框架使用起来不是很方便,React后面的版本会完善Suspense,让我们脱离框架使用Suspense加载数据。

React DOM Client

react dom 新增了react-dom/client,通过createRoot渲染的react组件将开启 Concurrent Mode

旧版本的ReactDom.render是不开启Concurrent Mode。增加这个api就是为了让大家平滑的升级到ReactV18.

import ReactDOM from 'react-dom';
import App from './App';

const container = document.getElementById('app');

// Initial render.
ReactDOM.render(<App tab="home" />, container);
import ReactDOM from "react-dom/client";
import App from "./App";

const rootElement = document.getElementById("root")!;
const root = ReactDOM.createRoot(rootElement);

root.render(<App />);

render callback

旧版本的ReactDom.render支持callback表示渲染完成。新版本的不再支持,需要在组件中使用useEffect处理

import ReactDOM from 'react-dom';
import App from './App';

const container = document.getElementById('app');

ReactDOM.render(container, <App tab="home" />, function() {
  // Called after inital render or any update.
  console.log('rendered').
});
import ReactDOMClient from 'react-dom/client';

function App({ callback }) {
  // Callback will be called when the div is first created.
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");

const root = ReactDOMClient.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);
unmountComponentAtNode
// Before
ReactDom.unmountComponentAtNode(container);

// After
root.unmount();
StrictMode

后面React会有个卸载的组件再次挂载时保持之前的state状态的功能(有点类似vue的keep-alive),<OffScreen />

这样会存在组件的多次 mountedunmounted。所以ReactV18的StrictMode在开发环境时会在mounted之后自动的unmountedmounted

但是ReactV18不开启Concurrent Mode的话,表现也是和ReactV17一致的。


新Hooks

useId
  • 功能: 生成全局唯一的id(字符串)。
  • 用法:
const id = useId();
useTransition
  • 功能:创建不阻塞UI的更新。
  • 用法:
import { useTransition } from 'react';

function TabContainer() {
  const [value, setValue] = useState('');
  const [isPending, startTransition] = useTransition();
  // ...
  startTransition(() => {
      setValue('xxx');
  })
}
  • 注意:
    • useTransition是个hook,只能用在组件。脱离组件的可以使用apistartTransition.
    • 返回的startTransition的callback函数只能是个同步函数。callback函数是会被立即执行的,只是setState的状态更新的低优先级的。
    • input的输入,不可以用transition更新。
useDeferredValue
  • 功能:获取延迟更新的state(类似debounce)
  • 用法: query发生变化时实时的,但是deferredValue是延迟更新。类似:在startTransition里面setState。useDeferredValue包装的数据更新渲染也是可打断的。
const [query, setQuery] = useState('');
const deferredValue = useDeferredValue(query);
  • 注意: useDeferredValue接收的参数,最好不要是每次渲染的时候生成的,这样会导致每次render的时候都需要更新。
const [list, setList] = useState([1, 2, 3]);
// 这种每次渲染的时候都是新的值,导致每次更新。useDeferredValue内部也是用Object.is进行比较新旧值的。
const deferredValue = useDeferredValue(list.map((v) => v + 1);

PS: useDeferredValue想达到优化效果还需要搭配 React.memo,负责每次text的变化,依然运行了render。

useSyncExternalStore
  • 功能:通过useSyncExternalStore可以将非react state的数据管理工具和React链接起来。
  • 用法:
    • subscribe: 监听函数,返回值是取消监听函数。
    • getSnapshot: 获取数据函数,返回的值就是useSyncExternalStore的返回值。
    • getServerSnapshot:服务端渲染使用。
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • 注意:
    • getSnapshot的返回值不能每次render返回一个新数据,这样会导致一直render。所以getSnapshot的返回值需要做好缓存。
    • subscribe发生变化的时候,React会调用之前的subscribe返回值,取消监听,然后再调用新的subscribe开始新的监听。所以subscribe最好也是不变的。

demo

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

export default function TodosApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
  return (
    <>
      <button onClick={() => todosStore.addTodo()}>Add todo</button>
      <hr />
      <ul>
        {todos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
    </>
  );
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];

export const todosStore = {
  addTodo() {
    todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
    emitChange();
  },
  subscribe(listener) {
    listeners = [...listeners, listener];
    return () => {
      listeners = listeners.filter(l => l !== listener);
    };
  },
  getSnapshot() {
    return todos;
  }
};

function emitChange() {
  for (let listener of listeners) {
    listener();
  }
}

总结:

  • useSyncExternalStore可以帮助把非React编写的数据管理和React组件链接起来。但是如果可以的话,还是建议使用useStateuseReducerreact-redux v8.0.0 已经使用useSyncExternalStore去实现了。
  • 根据提供的demo,可以轻松使用useSyncExternalStore实现之前想要的单例功能,只需要保证数据是单例的就可以了。单例demo
useInsertionEffect
  • 功能:用于css-in-js类库开发者,在 DOM mutations 之前触发。可以动态插入styles。
  • 用法:使用方式和useEffect一样,只是触发时机不同。
useInsertionEffect(setup, dependencies?)
// Inside your CSS-in-JS library
let isInserted = new Set();
function useCSS(rule) {
  useInsertionEffect(() => {
    // As explained earlier, we don't recommend runtime injection of <style> tags.
    // But if you have to do it, then it's important to do in useInsertionEffect.
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

function Button() {
  const className = useCSS('...');
  return <div className={className} />;
}


如何升级到React v18

渐进式更新

React v18的很多特性都是基于Concurrent ModeConcurrent Mode是可选择的。不开启Concurrent Mode很多功能都和React v17一样。

第一阶段:

操作:升级到ReactV18后,不使用createRootrender。

结果:

  • Concurrent Mode相关功能都不起作用
  • Supspense功能正常
  • StrictMode跟ReactV17表现一致
  • auto batching不起作用
  • 新增的hooks
    • ✅起作用useIduseSyncExternalStoreuseInsertionEffect
    • ❌不起作用useTransitionuseDeferredValue

第二阶段:

操作:升级到ReactV18后,使用createRootrender。 结果:

  • Concurrent Mode相关功能可用
  • Supspense功能正常
  • StrictMode功能正常,会unmounted后再mounted
  • auto batching功能正常
  • 新增的hooks
    • ✅起作用useIduseSyncExternalStoreuseInsertionEffect
    • ✅起作用useTransitionuseDeferredValue

但是不使用useTransitionuseDeferredValue的话,setState跟ReactV17功能一样。

第三阶段:

操作:根据功能,按需使用useTransitionuseDeferredValue,提升用户体验。

升级步骤
  1. 安装最新版本的reactreact-dom
npm i react@18.2.0 react-dom@18.2.0 -S
  1. 类库更新(因为@types/react and @types/react-dom的更新,不更新类库的话,ts都会报找不到children
  • antd从4.20.0开始支持 React 18,先升级到4.20.0
  npm i antd@4.20.0 -S
  • @testing-library更新,删除@testing-library/react-hooks
npm i @testing-library/react@13 -D 
npm uninstall @testing-library/react-hooks -D
  • @sentry/react更新
npm i @sentry/react -S
  • react-reduxreact-router更新
  npm i react-redux react-router react-router-dom@5 -S

有一些第三方的类库暂时是不支持React18的,但是因为React18用起来和React17一样,所以运行的时候是正常的。

  1. 升级types
npm i @types/react@18.0.28 @types/react-dom@18.0.11  -D
  1. 新版本的@types/react and @types/react-domFC移除了默认的children类型。通过 自动更新工具可以一键更新代码。
npx types-react-codemod preset-18 ./src ./projects ./packages
  1. 运行一下项目,可以正常允许,控制台会有一个warning,提醒你切换render到createRoot render。

  2. 切换render到createRoot render

注意事项

参考资料: