早在数月前,React 团队便预告了 React 19 的积极开发,并预计上半年发布。 4 月 25 日,React 终于发布了 v19 测试版。该版本主要面向各大库,以确保它们与 React 19 的兼容性。因此,建议开发者先升级至最新的稳定版 18.3.0,静待 React 19 的正式版发布。
function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, setIsPending] = useState(false); const handleSubmit = async () => { setIsPending(true); const error = await updateName(name); setIsPending(false); if (error) { setError(error); return; } redirect("/path"); }; // 堆代码 duidaima.com return ( <div> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>{error}</p>} </div> ); }在 React 19 中增加了对在转换中使用异步函数的支持,以自动处理挂起状态、错误、表单和乐观更新。例如,可以使用useTransition来自动处理挂起状态:
function UpdateName({}) { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); const handleSubmit = async () => { startTransition(async () => { const error = await updateName(name); if (error) { setError(error); return; } redirect("/path"); }) }; return ( <div> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>{error}</p>} </div> ); }异步转换会立刻将isPending状态设为true,发起异步请求,并在转换完成后将isPending设置为false。这样可以确保数据变化时,UI 仍然保持响应和交互性。在 Actions 功能的基础上,React 19 还推出了useOptimistic Hook 来简化乐观更新的管理,以及 React.useActionState Hook来处理 Actions 的常见场景。同时,在 react-dom 中,新增了<form> Actions 来自动管理表单操作,并提供了useFormStatus 来支持表单中 Actions 的常见需求。
function ChangeName({ name, setName }) { const [error, submitAction, isPending] = useActionState( async (previousState, formData) => { const error = await updateName(formData.get("name")); if (error) { return error; } redirect("/path"); } ); return ( <form action={submitAction}> <input type="text" name="name" /> <button type="submit" disabled={isPending}>Update</button> {error && <p>{error}</p>} </form> ); }下面就来看看这些新的 Actions 功能的作用和使用方法。
const [error, submitAction, isPending] = useActionState(async (previousState, newName) => { const error = await updateName(newName); if (error) { // 可以返回Action的任何结果,这里只返回错误信息。 return error; } // 处理成功的情况 });useActionState接受一个函数(即“Action”),并返回一个可调用的包装 Action。这之所以能够工作是因为Actions是可组合的。当调用包装后的Action时,useActionState将返回Action的最后一个结果作为数据,并将Action的挂起状态作为pending返回。
import { useFormStatus } from 'react-dom'; function DesignButton() { const { pending } = useFormStatus(); return <button type="submit" disabled={pending} />; }useFormStatus可以读取父级<form>的状态,就像该表单是一个Contextprovider 一样。
function ChangeName({currentName, onUpdateName}) { const [optimisticName, setOptimisticName] = useOptimistic(currentName); const submitAction = async formData => { const newName = formData.get("name"); setOptimisticName(newName); const updatedName = await updateName(newName); onUpdateName(updatedName); }; return ( <form action={submitAction}> <p>Your name is: {optimisticName}</p> <p> <label>Change Name:</label> <input type="text" name="name" disabled={currentName !== optimisticName} /> </p> </form> ); }在这个例子中,useOptimistic Hook 允许在updateName请求还在进行时,立即将输入框的值(即newName)设置为optimisticName,从而乐观地更新UI。如果更新成功,则通过onUpdateName回调函数来确认状态的更改;如果更新失败或发生错误,可以通过setOptimisticName回滚到原始状态currentName。
import { use } from 'react'; function Comments({ commentsPromise }) { // 使用use读取Promise,React会在Promise解析前挂起组件渲染 const comments = use(commentsPromise); return ( <div> {comments.map((comment) => ( <p key={comment.id}>{comment.text}</p> ))} </div> ); } function Page({ commentsPromise }) { // 当Comments组件因use挂起时,这里会显示Suspense的fallback内容 return ( <Suspense fallback={<div>加载中...</div>}> <Comments commentsPromise={commentsPromise} /> </Suspense> ); }同样可以使用use来读取上下文(Context),从而实现在特定条件下(例如早期返回之后)按需读取上下文数据。早期返回指的是在函数或方法中的某个点提前结束执行,并返回结果或退出,而不是继续执行剩余的代码。在函数内部,根据某些条件或逻辑,你可能会决定不需要继续执行后续的代码,此时可以使用return语句来立即退出函数。
import {use} from 'react'; import ThemeContext from './ThemeContext' function Heading({children}) { if (children == null) { return null; } // 堆代码 duidaima.com // 使用 useContext 在这里不会生效 ,因为存在早期返回。 const theme = use(ThemeContext); return ( <h1 style={{color: theme.color}}> {children} </h1> ); }use API 的调用仅限于渲染阶段,与 React 的 Hook 类似。然而,与 Hook 不同的是,use API 允许在条件语句中灵活调用。展望未来,React 团队计划进一步扩展 use API 的功能,提供更多在渲染时消费资源的方式。
function MyInput({placeholder, ref}) { return <input placeholder={placeholder} ref={ref} /> } //... <MyInput ref={ref} />新的函数组件将不再需要 forwardRef,React 团队将会发布一个代码转换工具来自动更新组件,以使用新的 ref 属性。在未来的版本中,将弃用并移除 forwardRef。
const ThemeContext = createContext(''); function App({children}) { return ( <ThemeContext value="dark"> {children} </ThemeContext> ); }这种新的语法更加简洁直观。为了方便开发者升级现有代码,React 团队将发布一个代码转换工具,能够自动将现有的 <Context.Provider> 转换为新的 <Context> 提供者。未来版本中,将逐步弃用 <Context.Provider>,以推动 React 社区向更加简化的语法过渡。
<input ref={(ref) => { // 创建 ref // 新增:返回一个清理函数,当元素从 DOM 中移除时重置 ref。 return () => { // ref 的清理工作 }; }} />当组件卸载时,React 将调用从 ref 回调函数中返回的清理函数。这适用于 DOM refs、类组件的 refs 以及 useImperativeHandle。由于引入了 ref 清理函数的机制,现在 TypeScript 将拒绝从 ref 回调函数中返回除清理函数以外的任何内容。为了避免这个问题,我们通常建议避免使用隐式返回,比如将赋值操作放在花括号中,如下所示:
<div ref={current => (instance = current)} />优化后的写法:
<div ref={current => { instance = current; }} />这种改变是因为 TypeScript 无法判断原始代码中返回的是否应该是清理函数,还是无意中的隐式返回值。通过将赋值操作明确地包裹在花括号中,确保了 ref 回调中不会意外地返回任何值,除非有意为之。为了自动化这种模式的转换,可以使用 no-implicit-ref-callback-return 规则进行代码转换。这将帮助你在升级 React 版本时更顺畅地处理 ref 相关的代码。
function Search({ deferredValue }) { // 在组件首次渲染时,返回 initialValue 作为 value。 // 随后,useDeferredValue 会在后台计划一次重渲染,使用 deferredValue 作为新的 value。 const value = useDeferredValue(deferredValue, { initialValue: '' }); return ( <Results query={value} /> ); }使用 initialValue 可以确保组件在首次渲染时能够立即显示一个占位值,而无需等待 deferredValue 的异步计算完成。随后,当 deferredValue 准备好时,useDeferredValue 会触发组件的后台重渲染,以显示最新的值。这有助于提升应用的响应性和用户体验。
function BlogPost({ post }) { return ( <article> <h1>{post.title}</h1> {/* 直接在组件内部定义元数据标签 */} <title>{post.title}</title> <meta name="author" content="Josh" /> <link rel="author" href="https://twitter.com/joshcstory/" /> <meta name="keywords" content={post.keywords.join(', ')} /> <p> Eee equals em-see-squared... </p> </article> ); }在这个BlogPost组件中,尽管<title>、<meta>和<link>标签被定义在了<article>内部,但React会在渲染时自动将它们移至文档的<head>区域。这种方式简化了元数据的管理,还增强了React应用在各种渲染模式下的兼容性和灵活性。
function ComponentOne() { return ( <Suspense fallback="loading..."> <link rel="stylesheet" href="foo" precedence="default" /> <link rel="stylesheet" href="bar" precedence="high" /> <article class="foo-class bar-class"> {...} </article> </Suspense> ) } function ComponentTwo() { return ( <div> <p>{...}</p> <link rel="stylesheet" href="baz" precedence="default" /> <-- will be inserted between foo & bar </div> ) }在服务端渲染过程中,React会将样式表包含在<head>标签中,以确保浏览器在加载完成前不会进行页面绘制。如果在已经开始流式传输后才发现样式表,React 将确保在客户端将样式表插入到<head>标签中,然后再展示依赖于该样式表的Suspense边界的内容。
function App() { return <> <ComponentOne /> ... <ComponentOne /> // 不会导致 DOM 中出现重复的样式表链接 </> }对于那些习惯于手动加载样式表的开发者来说,React 19 的这一改进为他们提供了一个便利的机会。现在,可以将样式表直接放在依赖它们的组件旁边,这不仅有助于提升代码的可读性和可维护性,使得开发者可以更加清晰地了解每个组件的样式依赖关系,而且还能够确保只加载真正需要的样式表。
function MyComponent() { return ( <div> <script async src="..." /> Hello World </div> ); } function App() { return ( <html> <body> <MyComponent /> ... <MyComponent /> // 不会导致DOM中出现重复的脚本 </body> </html> ); }在所有渲染环境中,异步脚本都将进行去重处理,因此即使多个不同的组件渲染了同一个脚本,React 也只会加载和执行该脚本一次。在服务端渲染中,异步脚本将被包含在<head>标签中,并优先于阻止绘制的更关键资源(如样式表、字体和图像预加载)之后加载。
import { prefetchDNS, preconnect, preload, preinit } from 'react-dom'; function MyComponent() { preinit('https://.../path/to/some/script.js', { as: 'script' }); // 提前加载并执行脚本 preload('https://.../path/to/font.woff', { as: 'font' }); // 预加载字体 preload('https://.../path/to/stylesheet.css', { as: 'style' }); // 预加载样式表 prefetchDNS('https://...'); // 当可能会从该主机请求资源但尚不确定时 preconnect('https://...'); // 当确定会从该主机请求资源但不确定具体资源时 }这些 API 调用会在渲染组件时生成相应的DOM标签,如下所示:
<html> <head> <!-- 链接根据其对页面加载的贡献程度进行优先级排序 --> <link rel="prefetch-dns" href="https://..."> <!-- DNS预获取 --> <link rel="preconnect" href="https://..."> <!-- 提前建立连接 --> <link rel="preload" as="font" href="https://.../path/to/font.woff"> <!-- 预加载字体 --> <link rel="preload" as="style" href="https://.../path/to/stylesheet.css"> <!-- 预加载样式表 --> <script async src="https://.../path/to/some/script.js"></script> <!-- 异步加载并执行脚本 --> </head> <body> ... </body> </html>通过利用这些API,开发者可以优化页面的初始加载速度,减少用户等待时间。同时,在客户端更新时,预取和预加载资源也能帮助加快导航速度,提升用户体验。无论是通过预加载字体和样式表来减少页面渲染阻塞,还是通过预取DNS和预连接来加速资源请求,这些API都为开发者提供了强大的工具,使资源加载更加智能和高效。