React 19 即將推出的四個全新 Hooks,很實用!
近日,React 團隊成員在社交平臺表示,團隊正在開發(fā) React v19 版本,并且沒有計劃發(fā)布 v18.3 版本。
圖片
React 19 預計將推出 4 個全新 Hooks,這些 Hooks 主要關注 React 中的兩個痛點:數(shù)據(jù)獲取和表單。 這些 Hooks 目前在 React 預覽版本中作為實驗性 API 提供,預計會成為 React 19 的一部分,但是最終發(fā)布之前,API 可能會有所變化。
新的 Hooks 包括:
- use
- useOptimistic
- useFormState
- useFormStatus
use
use 是一個實驗性 React Hook,它可以讓讀取類似于 Promise 或 context 的資源的值。
const value = use(resource);
官方文檔:https://zh-hans.react.dev/reference/react/use
use(Promise)
use 可以在客戶端進行“掛起”的 API。可以將一個 promise 傳遞給它,React 將會在該 promise 解決之前進行掛起。它的基本語法如下:
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
// ...
}
下面來看一個簡單的例子:
import * as React from 'react';
import { useState, use, Suspense } from 'react';
import { faker } from '@faker-js/faker';
export const App = () => {
const [newsPromise, setNewsPromise] = useState(() => fetchNews());
const handleUpdate = () => {
fetchNews().then((news) => {
setNewsPromise(Promise.resolve(news));
});
};
return (
<>
<h3>
新聞列表
<button onClick={handleUpdate}>刷新</button>
</h3>
<NewsContainer newsPromise={newsPromise} />
</>
);
};
let news = [...new Array(4)].map(() => faker.lorem.sentence());
const fetchNews = () =>
new Promise<string[]>((resolve) =>
// 使用 setTimeout 模擬數(shù)據(jù)獲取
setTimeout(() => {
// 每次刷新時添加一個標題
news.unshift(faker.lorem.sentence());
resolve(news);
}, 1000)
);
const NewsContainer = ({ newsPromise }) => (
<Suspense fallback={<p>請求中...</p>}>
<News newsPromise={newsPromise} />
</Suspense>
);
const News = ({ newsPromise }) => {
const news = use<string[]>(newsPromise);
return (
<ul>
{news.map((title, index) => (
<li key={index}>{title}</li>
))}
</ul>
);
};
在上面的例子中,每次刷新時,都會先顯示“請求中...”,請求到數(shù)據(jù)后進行展示:
圖片
官方文檔中,關于 <Suspense> 有一個警告:
目前尚不支持在不使用固定框架的情況下進行啟用 Suspense 的數(shù)據(jù)獲取。實現(xiàn)支持 Suspense 數(shù)據(jù)源的要求是不穩(wěn)定的,也沒有文檔。React 將在未來的版本中發(fā)布官方 API,用于與 Suspense 集成數(shù)據(jù)源。
在新版本中,use 可能就是用于與 Suspense 集成數(shù)據(jù)源的官方 API。
這個全新的use hook 與其他 Hooks 不同,它可以在循環(huán)和條件語句中像 if 一樣被調用。這意味著我們可能不再需要依賴像 TanStack Query 這樣的第三方庫在客戶端進行數(shù)據(jù)獲取。然而,這仍需進一步觀察,因為 Tanstack Query 的功能遠不止解析 Promise 這么簡單。
use(Context)
這個 use hook 也可以用來讀取 React Context。它與 useContext 作用完全相同,只是可以在循環(huán)(如 for)和條件語句(如 if)中調用。
import { use } from 'react';
function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}
這將簡化某些場景下的組件層級結構,因為在循環(huán)或條件語句中讀取 context,之前唯一的方法就是將組件一分為二。
在性能方面,這一改進也是巨大的進步,因為現(xiàn)在即使 context 發(fā)生變化,我們也可以有條件地跳過組件的重新渲染。
useOptimistic
useOptimistic Hook 允許在進行提交動作的同時,能夠樂觀地更新用戶界面,提升用戶體驗。其語法如下:
import { useOptimistic } from 'react';
function AppContainer() {
const [optimisticState, addOptimistic] = useOptimistic(
state,
// 更新函數(shù)
(currentState, optimisticValue) => {
// 合并并返回帶有樂觀值的新狀態(tài)
},
);
}
樂觀更新:一種更新應用程序中數(shù)據(jù)的策略。這種策略通常會先更改前端頁面,然后向服務器發(fā)送請求,如果請求成功,則結束操作;如果請求失敗,則頁面回滾到先前狀態(tài)。這種做法可以防止新舊數(shù)據(jù)之間的跳轉或閃爍,提供更快的用戶體驗。
下面來看一個添加購物車的例子:
import { useState, useOptimistic } from 'react';
const AddToCartForm = ({ id, title, addToCart, optimisticAddToCart }) => {
const formAction = async (formData) => {
optimisticAddToCart({ id, title });
try {
await addToCart(formData, title);
} catch (e) {
// 捕獲錯誤
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到購物車</button>
</form>
);
};
type Item = {
id: string;
title: string;
};
const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
購物車:
<ul>
{cart.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
<hr />
</>
);
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const [optimisticCart, optimisticAddToCart] = useOptimistic<Item[], Item>(
cart,
(state, item) => [...state, item]
);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);
return { id };
};
return (
<>
<Cart cart={optimisticCart} />
<AddToCartForm
id="1"
title="JavaScript權威指南"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高級程序設計"
addToCart={addToCart}
optimisticAddToCart={optimisticAddToCart}
/>
</>
);
};
在上面的例子中,將商品添到購物車時,會先在購物車列表看到剛剛添加的商品,而不必等到數(shù)據(jù)請求完成。這樣,用戶可以更快地看到更新后的購物車內(nèi)容,提供更加流暢的用戶體驗。
圖片
useFormState
在介紹這個 Hook 之前,先來看以下這個 Hook 使用的背景。
<form>
React 將引入一個新組件:<form>,它是創(chuàng)建用于提交信息的交互式控件,可以將一個函數(shù)作為action的屬性值。當用戶提交表單時,React 將自動調用此函數(shù),以執(zhí)行相應的操作。
<form actinotallow={handleSubmit} />
注意,如果在 React 18 中添加<form action>屬性,就會收到警告:
?? Warning: Invalid value for prop action on <form> tag. Either remove it from the element or pass a string or number value to keep it in the DOM.
這里的意思是,<form>標簽上的 prop action無效。要么從元素中刪除它,要么傳遞一個字符串或數(shù)字值以將其保留在 DOM 中。
而在新版本中,可以直接在<form>標簽上設置action屬性。例如,在上面的購物車例子中,:
const AddToCartForm = ({ id, title, addToCart }) => {
const formAction = async (formData) => {
try {
await addToCart(formData, title);
} catch (e) {
// 捕獲錯誤
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到購物車</button>
</form>
);
};
addToCart 函數(shù)并不是在服務器端執(zhí)行的,而是在客戶端(例如用戶的瀏覽器)上運行的。這個函數(shù)可以是一個異步函數(shù),如網(wǎng)絡請求,而不阻止其他代碼的執(zhí)行。通過使用addToCart函數(shù),開發(fā)者可以更簡單地處理React中的AJAX表單,例如在搜索表單中。然而,這可能還不足以完全擺脫像 React Hook Form 這樣的第三方庫,因為它們不僅處理表單提交,還包括驗證、副作用等多種功能。
看完這個新功能,下面就來看看這一部分要介紹的新 Hook:useFormState。
useFormState
useFormState 是一個可以根據(jù)某個表單動作的結果更新 state 的 Hook。
const [state, formAction] = useFormState(fn, initialState);
只有在表單提交觸發(fā) action 后才會被更新的值,如果該表單沒有被提交,該值會保持傳入的初始值不變。
例如,這可以用來顯示由表單操作返回的確認消息或錯誤消息。
import { useState } from 'react';
import { useFormState } from 'react-dom';
const AddToCartForm = ({ id, title, addToCart }) => {
const addToCartAction = async (prevState, formData) => {
try {
await addToCart(formData, title);
return '添加成功';
} catch (e) {
return "添加失?。嘿u完啦";
}
};
const [message, formAction] = useFormState(addToCartAction, null);
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<button type="submit">添加到購物車</button>
{message}
</form>
);
};
type Item = {
id: string;
title: string;
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
if (id === '1') {
setCart((cart: Item[]) => [...cart, { id, title }]);
} else {
throw new Error('Unavailable');
}
return { id };
};
return (
<>
<AddToCartForm
id="1"
title="JavaScript權威指南"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高級程序設計"
addToCart={addToCart}
/>
</>
);
};
效果如下:
圖片
注意:useFormState 需要從 react-dom 中導入,而不是從 react 中導入。
useFormStatus
useFormStatus 用于獲取上次表單提交的狀態(tài)信息。
const { pending, data, method, action } = useFormStatus();
它不接收任何參數(shù),會返回一個包含以下屬性的 status 對象:
- pending:布爾值。如果為 true,則表示父級 <form> 正在等待提交;否則為 false。
- data:包含父級 <form> 正在提交的數(shù)據(jù);如果沒有進行提交或沒有父級 <form>,它將為 null。
- method:字符串,可以是 'get' 或 'post'。表示父級 <form> 使用 GET 或 POST HTTP 方法 進行提交。默認情況下,<form> 將使用 GET 方法,并可以通過 method 屬性指定。
- action:一個傳遞給父級 <form> 的 action 屬性的函數(shù)引用。如果沒有父級 <form>,則該屬性為 null。如果在 action 屬性上提供了 URI 值,或者未指定 action 屬性,status.action 將為 null。
下面來繼續(xù)看購物車的例子,將商品添加到購物車成功前,禁用添加按鈕:
import { useState } from 'react';
import { useFormStatus } from 'react-dom';
const AddToCartForm = ({ id, title, addToCart }) => {
const formAction = async (formData) => {
try {
await addToCart(formData, title);
} catch (e) {
// 捕獲錯誤
}
};
return (
<form action={formAction}>
<h2>{title}</h2>
<input type="hidden" name="itemID" value={id} />
<SubmitButton />
</form>
);
};
const SubmitButton = () => {
const { pending } = useFormStatus();
return (
<button disabled={pending} type="submit">
添加到購物車
</button>
);
};
type Item = {
id: string;
title: string;
};
const Cart = ({ cart }: { cart: Item[] }) => {
if (cart.length == 0) {
return null;
}
return (
<>
購物車:
<ul>
{cart.map((item, index) => (
<li key={index}>{item.title}</li>
))}
</ul>
<hr />
</>
);
};
export const App = () => {
const [cart, setCart] = useState<Item[]>([]);
const addToCart = async (formData: FormData, title) => {
const id = String(formData.get('itemID'));
await new Promise((resolve) => setTimeout(resolve, 1000));
setCart((cart: Item[]) => [...cart, { id, title }]);
return { id };
};
return (
<>
<Cart cart={cart} />
<AddToCartForm
id="1"
title="JavaScript權威指南"
addToCart={addToCart}
/>
<AddToCartForm
id="2"
title="JavaScript高級程序設計"
addToCart={addToCart}
/>
</>
);
};
添加購物車時效果如下:
圖片
注意:useFormState 需要從 react-dom 中導入,而不是從 react 中導入。此外,它僅在父級表單使用 action 屬性時才有效。