React18+ 新特性
统计 React18+ 用法
- 15个 hook(
useMemo
|useCallback
、useReducer
|useState
、useRef
、useEffect
、useContext
、useTransition
|useDeferredValue
、useDebugValue
、useId
、useImperativeHandle
、useInsertionEffect
、useLayoutEffect
、useSyncExternalStore
) - 4个 component(
<Fragment>
、<Profiler>
、<StrictMode>
、<Suspense>
) - 5个 API(
createContext(defaultValue)
、forwardRef(render)
、lazy(load)
、memo(SomeComponent, arePropsEqual?)
、startTransition(scope)
)
1、setState自动批处理
在react17中,只有react事件会进行批处理,原生js事件、promise,setTimeout、setInterval不会
react18,将所有事件都进行批处理,即多次setState会被合并为1次执行,提高了性能,在数据层,将多个状态更新合并成一次处理(在视图层,将多次渲染合并成一次渲染)
2、正式支持Concurrent
Concurrent最主要的特点就是渲染是可中断的。以前是不可中断的,也就是说,以前React中的update是同步渲染,在这种情况下,一旦update开启,在任务完成前,都不可中断。
- 可中断(如:渲染): 对于React来说,任务可能很多,需要区分优先级,高优先级任务可以中断低优先级任务(基于fiber本身链表结构,可以记录任务的执行情况,便于恢复)
- (任务)被遗弃:React 可能会把一些更新标记为低优先级,以避免阻塞高优先级更新的渲染,从而提高用户体验。这意味着某些低优先级更新可能会被暂停、中止或完全丢弃,因为它们在后续更新到达之前已经过时。 如:在现在React18中,这个模糊查询相关的UI可以被当做transition。
- 支持状态的复用:某些情况下,比如用户走了,又回来,那么上一次的页面状态应当被保存下来,而不是完全从头再来。当然实际情况下不能缓存所有的页面,不然内存不得爆炸,所以还得做成可选的。目前,React正在用Offscreen组件来实现这个功能。嗯,也就是这关于这个状态复用,其实还没完成呢。不过源码中已经在做了: 另外,使用OffScreen,除了可以复用原先的状态,我们也可以使用它来当做新UI的缓存准备,就是虽然新UI还没登场,但是可以先在后台准备着嘛,这样一旦轮到它,就可以立马快速地渲染出来。
总结一下:Concurrent并不是API之类的新特性,但是呢,它很重要,因为它是React18大部分新特性的实现基础,包括Suspense、transitions、流式服务端渲染等。
3、createRoot 取代以前的 ReactDOM.render
//React 17
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
const root = document.getElementById("root")
ReactDOM.render(<App/>,root)
// 卸载组件
ReactDOM.unmountComponentAtNode(root)
// React 18
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
import Demo from "./Demo"
const root = ReactDOM.createRoot(document.getElementById("root"))
// 并发模式的渲染
root.render(<App/>)
root.render(<Demo/>)
// 卸载组件
root.unmount()
//React 17
import React from "react"
import ReactDOM from "react-dom"
import App from "./App"
const root = document.getElementById("root")
ReactDOM.render(<App/>,root)
// 卸载组件
ReactDOM.unmountComponentAtNode(root)
// React 18
import React from "react"
import ReactDOM from "react-dom/client"
import App from "./App"
import Demo from "./Demo"
const root = ReactDOM.createRoot(document.getElementById("root"))
// 并发模式的渲染
root.render(<App/>)
root.render(<Demo/>)
// 卸载组件
root.unmount()
以前 root 对象 只存在于React内部,现在用户也可以复用 root 对象了。
以前ReactDOM.render第三个参数是在更新完成执行的callback,而在React18中,这样的callback会建议放在useEffect中:
const root = createRoot(document.getElementById("root"));
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return jsx
}
root.render(<AppWithCallbackAfterRender/>);
const root = createRoot(document.getElementById("root"));
function AppWithCallbackAfterRender() {
useEffect(() => {
console.log('rendered');
});
return jsx
}
root.render(<AppWithCallbackAfterRender/>);
4、ssr 中的 ReactDOM.hydrate 也换成了新的 hydrateRoot
以上两个API目前依然支持,只是已经移入legacy模式,开发环境下会报warning。
5、Suspense
可以“等待”目标UI加载,并且可以直接指定一个加载的界面(像是个 spinner),让它在用户等待的时候显示。
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
<Suspense fallback={<Spinner />}>
<Comments />
</Suspense>
其实Suspense也早就出现在React中了,只不过之前功能有限。在React18中,背靠Concurrent模式,Suspense终于爆发了自己的光彩。
在概念上,Suspense有点像catch,只不过Suspense捕获的不是异常,而是组件的suspending状态,即挂载中。
显示 loading
import {useState, Suspense} from "react";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
import ErrorBoundaryPage from "./ErrorBoundaryPage";
const initialResource = fetchData();
export default function SuspensePage(props) {
const [resource, setResource] = useState(initialResource);
return (
<div>
<h3>SuspensePage</h3>
{/* 捕获异常 */}
<ErrorBoundaryPage fallback={<h1>网络出错了</h1>}>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
</ErrorBoundaryPage>
{/* 显示 loading */}
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
<button onClick={() => setResource(fetchData())}>refresh</button>
</div>
);
}
import {useState, Suspense} from "react";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
import ErrorBoundaryPage from "./ErrorBoundaryPage";
const initialResource = fetchData();
export default function SuspensePage(props) {
const [resource, setResource] = useState(initialResource);
return (
<div>
<h3>SuspensePage</h3>
{/* 捕获异常 */}
<ErrorBoundaryPage fallback={<h1>网络出错了</h1>}>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
</ErrorBoundaryPage>
{/* 显示 loading */}
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
<button onClick={() => setResource(fetchData())}>refresh</button>
</div>
);
}
错误处理
每当使用 Promises,大概率我们会用 catch() 来做错误处理。但当我们用 Suspense 时,我们不等待 Promises 就直接开始渲染,这时 catch() 就不适用了。这种情况下,错误处理该怎么进行呢?
在 Suspense 中,获取数据时抛出的错误和组件渲染时的报错处理方式一样——你可以在需要的层级渲染一个错误边界组件来“捕捉”层级下面的所有的报错信息。
import { Component } from "../whichReact";
export default class ErrorBoundaryPage extends Component {
state = { hasError: false, error: null };
// 此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
import { Component } from "../whichReact";
export default class ErrorBoundaryPage extends Component {
state = { hasError: false, error: null };
// 此生命周期会在后代组件抛出错误后被调用。 它将抛出的错误作为参数,并返回一个值以更新 state
static getDerivedStateFromError(error) {
return {
hasError: true,
error,
};
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
6、SuspenseList
SuspenseList在DebugReact中可用,也就是 DEV 环境下可以用。目前还未完成,预计18.X会正式支持,以下例子当做参考,以后也许会改变。
用于控制Suspense组件的显示顺序。
revealOrder 属性
Suspense 加载顺序
- together: 所有Suspense一起显示,也就是最后一个加载完了才一起显示全部
- forwards: 按照顺序显示Suspense
- backwards: 反序显示Suspense
tail 属性
是否显示fallback,只在 revealOrder 为 forwards 或者 backwards 时候有效
- hidden 不显示
- collapsed 轮到自己再显示
import {useState, Suspense, SuspenseList} from "react";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
import ErrorBoundaryPage from "./ErrorBoundaryPage";
const initialResource = fetchData();
export default function SuspenseListPage(props) {
const [resource, setResource] = useState(initialResource);
return (
<div>
<h3>SuspenseListPage</h3>
<SuspenseList tail="collapsed">
<ErrorBoundaryPage fallback={<h1>网络出错了</h1>}>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
</ErrorBoundaryPage>
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
</SuspenseList>
<button onClick={() => setResource(fetchData())}>refresh</button>
</div>
);
}
import {useState, Suspense, SuspenseList} from "react";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
import ErrorBoundaryPage from "./ErrorBoundaryPage";
const initialResource = fetchData();
export default function SuspenseListPage(props) {
const [resource, setResource] = useState(initialResource);
return (
<div>
<h3>SuspenseListPage</h3>
<SuspenseList tail="collapsed">
<ErrorBoundaryPage fallback={<h1>网络出错了</h1>}>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
</ErrorBoundaryPage>
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
</SuspenseList>
<button onClick={() => setResource(fetchData())}>refresh</button>
</div>
);
}
7、transition
React把update分成两种:
- Urgent updates 紧急更新,指直接交互,通常指的用户交互。如点击、输入等。这种更新一旦不及时,用户就会觉得哪里不对。
- Transition updates 过渡更新,如UI从一个视图向另一个视图的更新。通常这种更新用户并不着急看到(不中断交互)。
startTransition
startTransition 可以用在任何你想更新的时候。但是从实际来说,以下是两种典型适用场景:
- 渲染慢:如果你有很多没那么着急的内容要渲染更新。
- 网络慢:如果你的更新需要花较多时间从服务端获取。这个时候也可以再结合Suspense。
Button
import {
//startTransition,
useTransition,
} from "react";
export default function Button({refresh}) {
const [isPending, startTransition] = useTransition();
return (
<div className="border">
<h3>Button</h3>
<button
onClick={() => {
startTransition(() => {
refresh();
});
}}
disabled={isPending}>
点击刷新数据
</button>
{isPending ? <div>loading...</div> : null}
</div>
);
}
import {
//startTransition,
useTransition,
} from "react";
export default function Button({refresh}) {
const [isPending, startTransition] = useTransition();
return (
<div className="border">
<h3>Button</h3>
<button
onClick={() => {
startTransition(() => {
refresh();
});
}}
disabled={isPending}>
点击刷新数据
</button>
{isPending ? <div>loading...</div> : null}
</div>
);
}
import {useEffect, useState, Suspense} from "react";
import Button from "../components/Button";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
const initialResource = fetchData();
export default function TransitionPage(props) {
const [resource, setResource] = useState(initialResource);
// useEffect(() => {
// console.log("resource", resource); //sy-log
// }, [resource]);
return (
<div>
<h3>TransitionPage</h3>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
<Button
refresh={() => {
setResource(fetchData());
}}
/>
</div>
);
}
import {useEffect, useState, Suspense} from "react";
import Button from "../components/Button";
import User from "../components/User";
import Num from "../components/Num";
import {fetchData} from "../utils";
const initialResource = fetchData();
export default function TransitionPage(props) {
const [resource, setResource] = useState(initialResource);
// useEffect(() => {
// console.log("resource", resource); //sy-log
// }, [resource]);
return (
<div>
<h3>TransitionPage</h3>
<Suspense fallback={<h1>loading - user</h1>}>
<User resource={resource} />
</Suspense>
<Suspense fallback={<h1>loading-num</h1>}>
<Num resource={resource} />
</Suspense>
<Button
refresh={() => {
setResource(fetchData());
}}
/>
</div>
);
}
与setTimeout异同
在startTransition出现之前,我们可以使用setTimeout来实现优化。但是现在在处理上面的优化的时候,有了startTransition基本上可以抛弃setTimeout了,原因主要有以三点:
- 与 setTimeout 不同的是,startTransition 并不会延迟调度,而是会立即执行,
- startTransition 接收的函数是同步执行的,只是这个 update 被加了一个 “transitions" 的标记。而这个标记,React 内部处理更新的时候是会作为参考信息的。
- 相比于 setTimeout, 把一个 update 交给 startTransition 能够更早地被处理。而在于较快的设备上,这个过度是用户感知不到的
useTransition
在使用startTransition更新状态的时候,用户可能想要知道transition的实时情况,这个时候可以使用React提供的hook api useTransition。
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
import { useTransition } from 'react';
const [isPending, startTransition] = useTransition();
如果transition未完成,isPending值为true,否则为false。
Suspense与transitions结合
所谓提高用户体验,一个重要的准则就是保证UI的连续性,如下面的例子,如果此时我想把tab从‘photos’切换到‘comments’,但是Comments又没法立马渲染出来,这个时候不可避免地,就会Photos页面消失,显现Spinner的loading页面,等一会儿,Comments页面才姗姗来迟。
function handleClick() {
setTab('comments');
}
<Suspense fallback={<Spinner />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
function handleClick() {
setTab('comments');
}
<Suspense fallback={<Spinner />}>
{tab === 'photos' ? <Photos /> : <Comments />}
</Suspense>
从UI连续性上来说,这个中间出现的Spinner就已经破坏了连续性。而实际上,正常人的反应其实是没有那么快,短暂的延迟我们是感觉不到的。所以考虑到UI的连续性,上面的例子,交互可不可以修改一下,把上面页面的切换当做transitions,这样即使tab切换,但是依然短暂停留在Photos,之后再改变到Comments。
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
上面这个例子我们使用的是startTransition,如果需要知道pending状态,可以使用useTransition:
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
<Suspense fallback={<Spinner />}>
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{tab === 'photos' ? <Photos /> : <Comments />}
</div>
</Suspense>
const [isPending, startTransition] = useTransition();
function handleClick() {
startTransition(() => {
setTab('comments');
});
}
<Suspense fallback={<Spinner />}>
<div style={{ opacity: isPending ? 0.8 : 1 }}>
{tab === 'photos' ? <Photos /> : <Comments />}
</div>
</Suspense>
useDeferredValue
使得我们可以延迟更新某个不那么重要的部分
相当于参数版的transitions。
举例:如下图,当用户在输入框输入“书”的时候,用户应该立马看到输入框的反应,而相比之下,下面的模糊查询框如果延迟出现一会儿其实是完全可以接受的,因为用户可能会继续修改输入框内容,这个过程中模糊查询结果还是会变化,但是这个变化对用户来说相对没那么重要,用户最关心的是看到最后的匹配结果。
用法如下:
import {useDeferredValue, useState} from "react";
import MySlowList from "../components/MySlowList";
export default function UseDeferredValuePage(props) {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text);
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div>
<h3>UseDeferredValuePage</h3>
{/* 保持将当前文本传递给 input */}
<input value={text} onChange={handleChange} />
{/* 但在必要时可以将列表“延后” */}
<p>{deferredText}</p>
<MySlowList text={deferredText} />
</div>
);
}
import {useDeferredValue, useState} from "react";
import MySlowList from "../components/MySlowList";
export default function UseDeferredValuePage(props) {
const [text, setText] = useState("hello");
const deferredText = useDeferredValue(text);
const handleChange = (e) => {
setText(e.target.value);
};
return (
<div>
<h3>UseDeferredValuePage</h3>
{/* 保持将当前文本传递给 input */}
<input value={text} onChange={handleChange} />
{/* 但在必要时可以将列表“延后” */}
<p>{deferredText}</p>
<MySlowList text={deferredText} />
</div>
);
}
MySlowList:
import React, {memo} from "react";
function ListItem({children}) {
let now = performance.now();
while (performance.now() - now < 3) {}
return <div className="ListItem">{children}</div>;
}
export default memo(function MySlowList({text}) {
let items = [];
for (let i = 0; i < 80; i++) {
items.push(
<ListItem key={i}>
Result #{i} for "{text}"
</ListItem>
);
}
return (
<div className="border">
<p>
<b>Results for "{text}":</b>
</p>
<ul className="List">{items}</ul>
</div>
);
});
import React, {memo} from "react";
function ListItem({children}) {
let now = performance.now();
while (performance.now() - now < 3) {}
return <div className="ListItem">{children}</div>;
}
export default memo(function MySlowList({text}) {
let items = [];
for (let i = 0; i < 80; i++) {
items.push(
<ListItem key={i}>
Result #{i} for "{text}"
</ListItem>
);
}
return (
<div className="border">
<p>
<b>Results for "{text}":</b>
</p>
<ul className="List">{items}</ul>
</div>
);
});
8、新的Hooks
关于useTransition与useDeferredValue上面已经介绍过了,接下来说下React18其它的新Hooks,其中useSyncExternalStore与useInsertionEffect属于Library Hooks。也就是普通应用开发者一般用不到,这俩主要用于那些需要深度融合React模型的库开发,比如Recoil等。
useId
用于产生一个在服务端与Web端都稳定且唯一的ID,也支持加前缀,这个特性多用于支持ssr的环境下:
export default function NewHookApi(props) {
const id = useId();
return (
<div>
<h3 id={id}>NewHookApi</h3>
</div>
);
}
export default function NewHookApi(props) {
const id = useId();
return (
<div>
<h3 id={id}>NewHookApi</h3>
</div>
);
}
TIP
注意:useId产生的ID不支持css选择器,如querySelectorAll。
useSyncExternalStore
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
const state = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
此Hook用于外部数据的读取与订阅,可应用Concurrent。
基本用法如下:
import { useStore } from "../store";
import { useId, useSyncExternalStore } from "../whichReact";
export default function NewHookApi(props) {
const store = useStore();
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
return (
<div>
<h3>NewHookApi</h3>
<button onClick={() => store.dispatch({ type: "ADD" })}>{state}</button>
</div>
);
}
import { useStore } from "../store";
import { useId, useSyncExternalStore } from "../whichReact";
export default function NewHookApi(props) {
const store = useStore();
const state = useSyncExternalStore(store.subscribe, store.getSnapshot);
return (
<div>
<h3>NewHookApi</h3>
<button onClick={() => store.dispatch({ type: "ADD" })}>{state}</button>
</div>
);
}
useStore是我另外定义的,
export function useStore() {
const storeRef = useRef();
if (!storeRef.current) {
storeRef.current = createStore(countReducer);
}
return storeRef.current;
}
function countReducer(action, state = 0) {
switch (action.type) {
case "ADD":
return state + 1;
case "MINUS":
return state - 1;
default:
return state;
}
}
export function useStore() {
const storeRef = useRef();
if (!storeRef.current) {
storeRef.current = createStore(countReducer);
}
return storeRef.current;
}
function countReducer(action, state = 0) {
switch (action.type) {
case "ADD":
return state + 1;
case "MINUS":
return state - 1;
default:
return state;
}
}
这里的createStore用的redux思路:
export function createStore(reducer) {
let currentState;
let listeners = [];
function getSnapshot() {
return currentState;
}
function dispatch(action) {
currentState = reducer(action, currentState);
listeners.map((listener) => listener());
}
function subscribe(listener) {
listeners.push(listener);
return () => {
// console.log("unmount", listeners);
};
}
dispatch({ type: "TIANNA" });
return {
getSnapshot,
dispatch,
subscribe,
};
}
export function createStore(reducer) {
let currentState;
let listeners = [];
function getSnapshot() {
return currentState;
}
function dispatch(action) {
currentState = reducer(action, currentState);
listeners.map((listener) => listener());
}
function subscribe(listener) {
listeners.push(listener);
return () => {
// console.log("unmount", listeners);
};
}
dispatch({ type: "TIANNA" });
return {
getSnapshot,
dispatch,
subscribe,
};
}
useInsertionEffect
useInsertionEffect(didUpdate);
useInsertionEffect(didUpdate);
函数签名同useEffect,但是它是在所有DOM变更前同步触发。主要用于css-in-js库,往DOM中动态注入<style>
或者 SVG <defs>
。因为执行时机,因此不可读取refs。
function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
function Component() {
let className = useCSS(rule);
return <div className={className} />;
}
function useCSS(rule) {
useInsertionEffect(() => {
if (!isInserted.has(rule)) {
isInserted.add(rule);
document.head.appendChild(getStyleForRule(rule));
}
});
return rule;
}
function Component() {
let className = useCSS(rule);
return <div className={className} />;
}
TIP
注意:hooks的执行顺序先后为,useInsertionEffect 、useLayoutEffect、useEffect 。