React v18 有哪些新功能
Concurrent Mode
React v18一个重要的特性就是Concurrent Mode,提升应用用户体验。
和旧版本相比Concurrent Mode 的核心是 渲染可以被打断。
旧版本一次setState导致更新是不可以被打断的,一直到结果渲染的用户面前。
默认
setState都是不可打断的,通过hookuseTransition,包装的setState代表是低优先级的,可以被打断的更新。
什么是可被打断的渲染?
例子里面通过人为的长任务扩大render的时间,对于不可打断的例子,会发现一旦触发setState之后就会开始阻塞响应。
但是对于可以被打断的ReactV18版本,在长任务render的时候,是不会阻塞其他setState的响应的。
可被打断渲染的粒度
通过扩大单个组件的render时间,可以发现input的输入出现了卡顿。所以可以被打断渲染的粒度是组件级别的。单个组件的render是不可被打断的。
低优先级一直被打断会发生什么
一直在input上输入内容后setState,低优先级的任务会被一直打断,会发现一段时间后会强制执行低优先级的任务,阻塞相应。
高低优先级运行顺序
本质上React还是单线程,只是通过不同的优先级调度去实现并发渲染。
新功能
automatic batching
React V18之前,只有在被react包装的事件里面,setState是异步更新的。对于promise、setTimeout、原生事件的绑定里面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的优势有
Transitions包裹的函数是立即执行的,只是状态更新是延迟的,setTimout整体是延迟执行。- 耗时长的render依然会阻塞
setTimout的执行。
Questions about specifics of Concurrent scheduling
Suspense
ReactV18 可以通过Suspense去做数据请求。搭配一些第三方框架Relay、Next.js、Remix、Hydrogen。
不久的将来,React会完善Suspense的功能,不依赖框架就可以方便的处理数据请求。
当前我们都是在code-split的时候搭配 React.lazy和Suspense去进行代码加载。但是React对于Suspense的定位是可以处理任何异步的功能(eg: 加载数据、加载资源等)。
Suspense的设计类似于 throw 和 catch。被throw的错误都是被离得最近的catch捕获。只不过Suspense捕获的是promise。
<Suspense fallback={<PageGlimmer />}>
<RightColumn>
<ProfileHeader />
</RightColumn>
<LeftColumn>
<Suspense fallback={<LeftColumnGlimmer />}>
<Comments />
<Photos />
</Suspense>
</LeftColumn>
</Suspense>
ProfileHeader组件没有准备好的时候,会展示fallback PageGlimmer。但是Comments、Photos组件没有准备好的时候,展示的是LeftColumnGlimmer不会影响RightColumn的展示。
注意:
Suspense只能捕获到组件的异步数据加载,对于useEffect和 event handler里面的trow promise是做不到捕获的。跟ErrorBoundary一样,只能捕获渲染过程中的
使用方式:
- 同时展示内容,子组件内部都没有
Suspense,最外层的Suspense只有在所有所有子组件都准备好之后才会展示内容(跟Promise.all一样)。demo
<Suspense fallback={<Loading />}>
<Biography />
<Panel>
<Albums />
</Panel>
</Suspense>
- 嵌套加载,
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 />。
这样会存在组件的多次 mounted 和 unmounted。所以ReactV18的StrictMode在开发环境时会在mounted之后自动的unmounted再mounted。
但是ReactV18不开启Concurrent Mode的话,表现也是和ReactV17一致的。
新Hooks
useId
- 功能: 生成全局唯一的id(字符串)。
- 用法:
const id = useId();
- 注意:
useId的返回值,不应该用作组件的key。key应该根据数据去生成
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最好也是不变的。
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组件链接起来。但是如果可以的话,还是建议使用useState和useReducer。react-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 Mode,Concurrent Mode是可选择的。不开启Concurrent Mode很多功能都和React v17一样。
第一阶段:
操作:升级到ReactV18后,不使用createRootrender。
结果:
- ❌
Concurrent Mode相关功能都不起作用 - ✅
Supspense功能正常 - ❌
StrictMode跟ReactV17表现一致 - ❌
auto batching不起作用 - 新增的hooks
- ✅起作用
useId、useSyncExternalStore、useInsertionEffect - ❌不起作用
useTransition、useDeferredValue
- ✅起作用
第二阶段:
操作:升级到ReactV18后,使用createRootrender。
结果:
- ✅
Concurrent Mode相关功能可用 - ✅
Supspense功能正常 - ✅
StrictMode功能正常,会unmounted后再mounted - ✅
auto batching功能正常 - 新增的hooks
- ✅起作用
useId、useSyncExternalStore、useInsertionEffect - ✅起作用
useTransition、useDeferredValue
- ✅起作用
但是不使用useTransition、useDeferredValue的话,setState跟ReactV17功能一样。
第三阶段:
操作:根据功能,按需使用useTransition、useDeferredValue,提升用户体验。
升级步骤
- 安装最新版本的
react和react-dom
npm i react@18.2.0 react-dom@18.2.0 -S
- 类库更新(因为
@types/reactand@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-redux、react-router更新
npm i react-redux react-router react-router-dom@5 -S
有一些第三方的类库暂时是不支持React18的,但是因为React18用起来和React17一样,所以运行的时候是正常的。
- 升级types
npm i @types/react@18.0.28 @types/react-dom@18.0.11 -D
- 新版本的
@types/reactand@types/react-dom,FC移除了默认的children类型。通过 自动更新工具可以一键更新代码。
npx types-react-codemod preset-18 ./src ./projects ./packages
-
运行一下项目,可以正常允许,
控制台会有一个warning,提醒你切换render到createRoot render。 -
切换render到createRoot render
注意事项
参考资料: