React 18 推出了一些令人兴奋的新改进。一年前宣布 React 18 时,团队承诺逐步采用策略。 现在,一年后,可以应用程序升级到最新版本。
总而言之,它还带来了开箱即用的性能改进,包括默认更多的批处理,这消除了在应用程序或库代码中手动批处理更新的需要。
本文一起深入了解 React 18 带来的一些最重要的新变化。
createRoot
启用 React 18 中的并发功能。若不使用它,应用程序将像在 React 17 上一样运行。
/** Before */
import { render } from "react-dom";
const container = document.getElementById("app");
render( , container);
/** After */
import { createRoot } from "react-dom/client";
const container = document.getElementById("app");
const root = createRoot(container);
root.render( );
为确保您正确迁移应用程序,请尝试启用严格模式:
import React from "react";
import { createRoot } from "react-dom/client";
function App() {
return (
);
}
const container = document.getElementById("app");
const root = createRoot(container);
root.render( );
此外,如果使用 hydrate 进行 “hydration” 的服务端渲染,可以升级到 hydraRoot:
/** Before */
import { hydrate } from "react-dom";
const container = document.getElementById("app");
hydrate( , container);
/** After */
import { hydrateRoot } from "react-dom/client";
const container = document.getElementById("app");
const root = hydrateRoot(container, );
/** 与 createRoot 不同的是这里不需要单独的 root.render() 调用。*/
自动批处理意味着 React 现在将批处理您在组件中进行的更新。 批处理可以防止不必要的组件渲染。
/** Before: 只有 React events 会被批处理 */
setTimeout(() => {
/** React 将渲染两次,每次状态更新一次(无批处理) */
setSize((oldSize) => oldSize + 1);
setOpen((oldOpen) => !oldOpen);
}, 1000);
/** After: 在 timeouts、promise、native 事件处理程序或任何其他事件被批处理 */
setTimeout(() => {
/** React 只会在最后重新渲染一次(即批处理)*/
setSize((oldSize) => oldSize + 1);
setOpen((oldOpen) => !oldOpen);
}, 1000);
前提是使用 createRoot 而不是 render时。 如果组件中不需要自动批处理,可以随时使用 flushSync 选择退出:
import { flushSync } from "react-dom";
function handleSubmit() {
/** React 将渲染两次,每次状态更新一次(无批处理) */
flushSync(() => {
setSize((oldSize) => oldSize + 1);
});
flushSync(() => {
setOpen((oldOpen) => !oldOpen);
});
}
对 setCounter 和 setFlag 的调用将立即尝试更新 DOM,而不是一起进行批处理。仅此一项新功能就可以改变应用程序的性能。只需更改为 createRoot 挂载应用程序即可使用。
React 18 引入了一个新的 transition API。 transition是 React 中的一个新概念,用于区分紧急和非紧急更新。
比如一个具有搜索功能的页面。 将文本添加到输入字段后,希望看到该文本立即显示在那里。 这是一个紧急更新。 但是,当您键入时,立即显示用户搜索结果并不紧迫。 相反,开发人员通常会在显示搜索结果之前对用户的输入进行去抖动或限制。因此,在输入字段中输入或单击过滤器按钮是一项紧急更新。 显示搜索结果不是紧急更新,它被视为过渡更新。 也可以使用名为 useTransition 的 hook 来获取过渡的标志,如下所示:
import { startTransition } from "react";
/** 紧急:显示输入的内容 */
setInputValue(newInputValue);
/** 将内部的任何状态更新标记为 transitions 并将它们标记为非紧急 */
startTransition(() => {
/** transition:显示结果 */
setSearchQuery(newInputValue);
});
包含在 startTransition 中的更新被视为非紧急更新,如果出现更紧急的更新(如点击或按键),则 startTransition 中的更新会被中断。假设转换被用户打断(例如,通过连续键入多个字符)。 在这种情况下,React 将丢弃未完成的陈旧渲染工作,仅渲染最新更新。
function App() {
const [isPending, startTransition] = useTransition();
const [count, setCount] = useState(0);
function handleClick() {
startTransition(() => {
setCount((oldCount) => oldCount + 1);
});
}
return (
当前次数: {count}
{isPending && }
);
}
这个过程中,React 将做三件事情:
Suspense 现在在服务器上可用。 以前,它可以在客户端使用 React.lazy 进行代码拆分。 但是现在,可以在组件 “Suspense” 时使用某种占位符。Suspense 意味着组件还没有准备好渲染 —— 它可能缺少数据或代码。如果使用 Suspense,服务器上的慢速渲染组件将不再阻碍整个页面。如果一个组件 Suspense,它上层最近的 Suspense 组件会“捕获”它,无论中间有多少组件。
}>
}>
在上面的例子中,如果 ProfileHeader Suspense,那么整个页面将被 PageSkeleton 替换。但是,如果 Comments 组件或 Photos组件 Suspense,它们都将被 LeftColumnSkeleton 替换。
这使得可以根据可视化 UI 设计的粒度安全地添加和删除 Suspense 边界,而不必担心可能依赖于异步代码和数据的组件。可以在关于 SSR Suspense 的详细 GitHub 讨论中了解更多信息。
useId
是一个新的 hook,用于在客户端和服务器上生成唯一 ID,同时避免 hydration 不匹配。
function CodeOfConductField() {
const id = useId();
return (
<>
>
);
}
useTransition 是新的 hook,返回转换的挂起状态的有状态值,以及启动它的函数。
const [isPending, startTransition] = useTransition();
startTransition 允许将回调中的更新标记为 transiton;
isPending 指示 transiton 何时处于 active 状态:
useDeferredValue 允许推迟重新渲染树的非紧急部分。它类似于去抖动或节流,但有一些优点。 没有固定的时间延迟,因此 React 将在第一次渲染反映在屏幕上后立即尝试延迟渲染。 延迟渲染是可中断的,不会阻止用户输入。比如:
function SearchResults() {
const query = useSearchQuery("");
const deferredQuery = useDeferredValue(query);
/** 告诉 React 只有在 deferredQuery 改变时才重新渲染,而不是 query改变时 */
const suggestionResults = useMemo(
() => ,
[deferredQuery]
);
return (
<>
{suggestionResults}
>
);
}
上述例子中,SearchSuggestions 组件将仅在 deferredQuery 更新时重新呈现。 为了将所有内容联系在一起,当 SearchSuggestions suspended 时,我们会看到“正在加载搜索结果...”的文本。
useSyncExternalStore 是用于以与选择性水合和时间切片等并发渲染功能兼容的方式从外部数据源读取和订阅的 hook。此 hook 适用于维护第三方库的作者,通常不用于应用程序代码。
useInsertionEffect 的签名与 useEffect 相同,但它在所有 DOM 突变之前同步触发。 这个 hook 用于在 useLayoutEffect 中读取布局之前将样式注入 DOM。 它无权访问 refs,也无法安排更新。useInsertionEffect 仅限于 css-in-js 库作者。
1. 可以渲染 undefined,
即如果从组件返回 undefined,React 不再抛出错误。
2. React 现在依赖于现代浏览器功能,包括Promise
、Symbol
和Object.assign.
因此,如果要支持旧版浏览器和设备(例如 Internet Explorer),请考虑在捆绑的应用程序中包含一个全局 polyfill。