React在2024年12月正式发布react 19的稳定版,那么19版本react都更新了什么内容呢,接下来小编会以案例的形式带大家了解下这些更新点。
Actions并不是一个api,而是一个概念词汇。
按照惯例,使用异步过渡的函数被称为 “Actions”
Actions 自动为你管理数据提交:
Action必须在startTransition内部使用。
先让我们复习一下startTransition的用法吧。
场景:tab页面切换,Home/Article/About,其中Article渲染页面非常卡顿,当切换到Article时,由页面卡顿,当再切换到其他页面时,会出现切换无效的问题。
tsximport { memo, useState,useTransition } from "react"
interface TabButtonProp {
isActive: boolean
children: React.ReactNode
onClick: () => void
}
const TabContainer = () => {
const [tab, setTab] = useState<"home" | "article" | "about">("home");
const [isPending,startTransition] = useTransition()
const selectTab = (newTab: "home" | "article" | "about") => {
startTransition(() => {
setTab(newTab);
})
};
return (
<div>
<div>
<TabButton isActive={tab === "home"} onClick={() => selectTab("home")}>
Home
</TabButton>
<TabButton
isActive={tab === "article"}
onClick={() => selectTab("article")}
>
{ isPending ? 'loading...' : 'Article'}
</TabButton>
<TabButton
isActive={tab === "about"}
onClick={() => selectTab("about")}
>
About
</TabButton>
</div>
{tab === "home" && <HomeTab />}
{tab === "article" && <ArticleTab />}
{tab === "about" && <AboutTab />}
</div>
);
}
const TabButton = ({ children, isActive, onClick }: TabButtonProp) => {
const btnStyle = {
backgroundColor: isActive ? "#4096ff" : "#fff",
border: "1px solid #999",
padding: "8px 16px",
margin: "0 4px",
cursor: "pointer",
display: "inline-block",
fontSize: "16px",
fontWeight: isActive ? "bold" : "normal",
color: isActive ? "#fff" : "#000",
};
return (
<button style={btnStyle} onClick={onClick}>
{children}
</button>
);
};
const HomeTab = () => {
return <h2>Welcome to Home</h2>;
}
const AboutTab = () => {
return <h2>About Us</h2>;
}
const ArticleTab = () => {
return (
<ul>
{new Array(100).fill(0).map((item, index) => {
return <SlowArticle key={index} index={index} />;
})}
</ul>
);
}
// 模拟一个渲染耗时的组件
const SlowArticle = memo(({ index }: {index:number}) => {
const startTime = performance.now();
while (performance.now() - startTime < 10) {} // 模拟慢操作
return <li>文章:{index + 1}</li>;
});
export default TabContainer;
Actions Demo
tscimport { useState, useTransition } from "react"; const ActionsDemo = () => { const [name, setName] = useState(""); const [error, setError] = useState(null); const [isPending, startTransition] = useTransition(); const handleSubmit = () => { startTransition(async () => { // react 19 支持异步函数,而在react8中必须是同步函数 const error = await updateName(name); if (error) { setError(error); return; } }); }; return ( <div> <input value={name} onChange={(event) => setName(event.target.value)} /> <button onClick={handleSubmit} disabled={isPending}> Update </button> {error && <p>{error}</p>} </div> ); }
useActionState 是一个可以根据某个表单动作的结果更新 state 的 Hook。
const [state, formAction, isPending] = useActionState(fn, initialState, permalink?);
tsximport { startTransition, useActionState, useState } from "react";
const UseActionStateDemo = () => {
const [name, setName] = useState("");
// formData就是调用submitHandle时传递的参数
const action = async (previousState, formData) => {
// ajax请求接口更新数据
const resp = await updateName(formData);
if (resp.code == 0) {
return {
success: true,
data: resp.data,
}; // 无论成功还是失败都必须有返回值,这个返回值会自动更新到state,也作为下一次的previousState
} else {
return {
success: false,
message: resp.message,
};
}
};
const [state, submitHandle, isPending] = useActionState(action, null);
return (
<div>
<input value={name} onChange={(e) => setName(e.target.value)} />
<button
disabled={isPending}
onClick={() => {
startTransition(() => {
// 注意外面必须使用startTransition包裹
submitHandle(name);
});
}}
>
提交
</button>
<h1>{state}</h1>
</div>
);
}
tsximport {useState,useActionState,startTransition} from 'react'
function App(){
// formData:FormData
const action = async (previousState, formData) => {
const resp = await updateName(formData)
if(resp.code == 0){
return {
success: true,
data: resp.data
} // 无论成功还是失败都必须有返回值,这个返回值会自动更新到state,也作为下一次的previousState
}else{
return {
success: false,
message: resp.message
}
}
}
const [state, submitHandle,isPending] = useActionState(action, null);
return (
<div>
<form action={submitHandle}>
<input name="name1"/>
<button disabled={isPending} type="submit">提交</button>
</form>
<h1>{state}</h1>
</div>
)
}
todoList案例
tsximport {useState,useOptimistic,startTransition} from 'react'
const fakeApi = (task) => {
return new promise((resolve,reect) => {
settimeout(() => {
if(Math.random() > 0.5){
resolve(`任务【${task}】添加成功`)
}else{
reject('任务添加失败')
}
},1000)
})
}
function TaskList(){
const [tasks,setTasks] = useState([]) //待办事项列表
const [optimisticState, addOptimistic] = useOptimistic(state, (currentTasks,newTask) => {
return [...currentTasks,newTask] //返回一个新的乐观状态
});
const addTask = (task) => {
startTransition(async() => {
// 在发起异步任务之前,先更新乐观值,如果请求失败了,会自动回滚乐观状态
addOptimistic(task)
// 掉接口,
await fakeApi(task)
// 更新真实状态
setTasks((currentTask) => {
return [...currentTask,task]
})
})
}
return (
<div>
<h1>待办事项列表</h1>
<ul>
{
optimisticState.map((task,index) => (<li key={index}>{task}</li>))
}
</ul>
</div>
)
}
useFormStatus 读取离它最近的那个 的状态,就像表单是一个 Context 提供者一样
tsximport {useFormStatus} from 'react-dom';
async function submitForm(){
return new Promise((res) => setTimeout(res,1000))
}
function Submit(){
// 获取父组件Form的状态
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit">
{pending ? '提交中...' : '提交'}
</button>
)
}
function Form({action}){
return (
<form action={action}>
<input name="message" />
<Submit />
</form>
)
}
function App(){
return <Form action={submitForm} />
}
use 是一个 React API,它可以让你读取类似于 Promise 或 context 的资源的值。
tsximport { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
const theme = use(ThemeContext);
// ...
}
与 React Hook 不同的是,可以在循环和条件语句(如 if)中调用 use。但需要注意的是,调用 use 的函数仍然必须是一个组件或 Hook。
当使用 Promise 调用 use API 时,它会与 Suspense 和 错误边界 集成。当传递给 use 的 Promise 处于 pending 时,调用 use 的组件也会 挂起。如果调用 use 的组件被包装在 Suspense 边界内,将显示后备 UI。一旦 Promise 被解决,Suspense 后备方案将被使用 use API 返回的数据替换。如果传递给 use 的 Promise 被拒绝,将显示最近错误边界的后备 UI。
当 context 被传递给 use 时,它的工作方式类似于useContext。而 useContext 必须在组件的顶层调用,use 可以在条件语句如 if 和循环如 for 内调用。相比之下,use 比 useContext更加灵活。 use 返回传递的 context 的 context 值。React 会搜索组件树并找到 最接近的 context provider 以确定需要返回的 context 值。
在react18中,如果想在父组件中获取子组件的ref,一般通过forwardRef实现。
案例点击按钮让子组件获取焦点
tsximport { forwardRef, useRef } from "react"
function RefDemo() {
const inputref = useRef<HTMLInputElement>(null)
const focusInput = () => {
inputref.current?.focus()
}
return (
<div>
<h1>我是父组件</h1>
<MyInput ref={ inputref} />
<button onClick={focusInput}>提交</button>
</div>
);
}
const MyInput = forwardRef<HTMLInputElement,any>((props,ref) => {
return <div>
<h1>我是子组件</h1>
<input ref={ref} type="text"/>
</div>
})
export default RefDemo
在react19中,ref可以作为props的属性
tsximport {useRef } from "react"
function RefDemo() {
const inputref = useRef<HTMLInputElement>(null)
const focusInput = () => {
console.log(inputref)
inputref.current?.focus()
}
return (
<div>
<h1>react19</h1>
<MyInput ref={ inputref} />
<button onClick={focusInput}>提交</button>
</div>
);
}
const MyInput = (props) => {
return <div>
<h1>我是子组件</h1>
<input ref={props.ref} type="text"/>
</div>
}
export default RefDemo
React 19 引入了更简洁的 Context 写法,现在可以直接使用 <Context> 代替 <Context.Provider>
tsxconst ThemeContext = createContext('');
function App({children}) {
return <ThemeContext value="dark">{children}</ThemeContext>;
}
这将使得在 ref 改变时执行清理操作变得更加容易(允许在组件卸载时自动执行清理逻辑)
场景: 一个按钮可以被另外一个按钮控制显示和隐藏,如果显示动态的添加click事件,隐藏时移除事件
在react8中的做法如下
tsximport { useEffect, useRef, useState } from "react"
function RefsDemo() {
const [showBtn, setShowBtn] = useState(true)
const btnRef = useRef<HTMLButtonElement>(null)
useEffect(() => {
if (btnRef.current) {
console.log("Button 2 addEventListener");
const handler = () => alert("Button 2 clicked");
btnRef.current.addEventListener("click", handler);
return () => {
console.log("Button 2 removeEventListener");
btnRef.current?.removeEventListener("click", handler);
};
}
},[showBtn])
return (
<div>
<h1>raect 18</h1>
<button onClick={() => setShowBtn(!showBtn)}>点击切换按钮2状态</button>
<br></br>
{
showBtn && <button ref={ btnRef}>按钮2</button>
}
</div>
);
}
export default RefsDemo
在react 19版本中的做法如下
tsxfunction RefsDemo19() {
const [showBtn, setShowBtn] = useState(true);
const setRef = (ref) => {
if (ref) {
console.log("Button 2 addEventListener");
const handler = () => alert("Button 2 clicked");
ref.addEventListener("click", handler);
return () => {
console.log("Button 2 removeEventListener");
ref?.removeEventListener("click", handler);
};
}
}
return (
<div>
<h1>raect 19</h1>
<button onClick={() => setShowBtn(!showBtn)}>点击切换按钮2状态</button>
<br></br>
{showBtn && <button ref={setRef}>按钮2</button>}
</div>
);
}
tsxfunction 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} />
<p>
Eee equals em-see-squared...
</p>
</article>
);
}
样式表,无论是外部链接的 () 还是内联的 (...),都需要在 DOM 中进行精确的定位,因为样式优先级规则。构建一个允许在组件内部进行组合的样式表功能是困难的,所以用户通常要么将所有的样式远离可能依赖它们的组件加载,要么使用一个封装了这种复杂性的样式库。
在 React 19 中,我们正在解决这个复杂性,并提供更深入的集成到客户端的并发渲染和服务器的流式渲染,内置支持样式表。如果你告诉 React 你的样式表的 precedence,它将管理样式表在 DOM 中的插入顺序,并确保在显示依赖于这些样式规则的内容之前加载样式表(如果是外部的)。
tsxfunction 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>
)
}
在 HTML 中,普通脚本 (
在 React 19 中,我们通过允许你在组件树的任何位置,即实际依赖脚本的组件内部,渲染它们,从而为异步脚本提供了更好的支持,无需管理脚本实例的重新定位和去重。
tsxfunction MyComponent() {
return (
<div>
<script async={true} src="..." />
Hello World
</div>
)
}
function App() {
<html>
<body>
<MyComponent>
...
<MyComponent> // won't lead to duplicate script in the DOM
</body>
</html>
}
在初始文档加载和客户端更新时,尽早告诉浏览器它可能需要加载的资源,可以显著提高页面性能。
React 19 包含了一些新的 API,用于加载和预加载浏览器资源,使得构建不受资源加载效率影响的优秀体验变得尽可能容易。
tsximport { prefetchDNS, preconnect, preload, preinit } from 'react-dom'
function MyComponent() {
preinit('https://.../path/to/some/script.js', {as: 'script' }) // loads and executes this script eagerly
preload('https://.../path/to/font.woff', { as: 'font' }) // preloads this font
preload('https://.../path/to/stylesheet.css', { as: 'style' }) // preloads this stylesheet
prefetchDNS('https://...') // when you may not actually request anything from this host
preconnect('https://...') // when you will request something but aren't sure what
}
上面的代码渲染的结果如下
html<!-- the above would result in the following DOM/HTML -->
<html>
<head>
<!-- links/scripts are prioritized by their utility to early loading, not call order -->
<link rel="prefetch-dns" href="https://...">
<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 可以通过将像字体这样的额外资源的发现从样式表加载中移出来,优化初始页面加载。它们还可以通过预取预期导航使用的资源列表,然后在点击或甚至悬停时积极预加载这些资源,使客户端更新更快。


本文作者:繁星
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!