由於
use
是比較新的 hook 目前可參考資料不多,文章中有些內容可能不是特別準確或者存在錯誤,若有任何錯誤歡迎在評論指出,我會馬上修正。
什麼是 use?
use
是 React 18 新出的一個 hook,目前只能在 canary 和 experimental 渠道中使用。
這個 hook 可以直接用來讀取 promise 或 context 的值。
它的出生是因為之前基於 Suspense 的 data fetching API 提案中,React 沒有內建的方法來讀取非同步值,因此通常會出現取值和渲染之間不必要的耦合。
一個簡單的使用例子:
import { use } from 'react'
import { ThemeContext } from '@context'
const MessageComponent = ({ msgPromise }) => {
const msg = use(msgPromise)
const theme = use(ThemeContext)
...
}
注意:
use
和其他 hook 一樣,需要在 React 組件或者 hook 函數內部調用。- 不能在 try-catch 中調用
use
,可以用ErrorBoundary
或者Promise.catch
來處理 rejected promise。 use
會在數據獲取到後重新渲染組件。
如果要試用
use
的話,先將 react 更新成 canary 版本https://react.dev/community/versioning-policy#all-release-channelsnpm update react@canary react-dom@canary
use 與其他 hook 的區別
根據官方文檔的說明,use
與其他 hook 最大的區別在調用位置。
use
可以在循環、條件判斷語句中調用,而其他 hook 不建議這麼做(可以,但最好不要)。
比如:
function HorizontalRule({ show }) {
if (show) {
const theme = use(ThemeContext);
return <hr className={theme} />;
}
return false;
}
本來看官方文檔的說明以為是其他 hook 並「不能」在循環、條件判斷語句中調用,但實際測試後發現並不會有錯誤,比如下面這個例子使用 useContext
照樣可以正常讀取:
function HorizontalRule({ show }) {
if (show) {
const theme = useContext(ThemeContext);
return <hr className={theme} />;
}
return false;
}
也可以在 if 中的 for 迴圈中使用:
import React from "react";
import Context from "./context";
import Todos from "./Todos";
const App = () => {
return (
<Context.Provider value={{ message: "測試" }}>
<Todos show={true} />
</Context.Provider>
);
};
export default App;
import { useContext } from "react";
import context from "./context";
let str = "";
const Todos = ({ show }) => {
if (show) {
for (let i = 0; i < 2; i++) {
const { message } = useContext(context);
str += message;
}
return <p>{str}</p>;
}
return <p>TODOS</p>;
};
export default Todos;
在查看了舊的官方文檔後大致了解不建議將 hook 放在迴圈、判斷語句中的原因,因為 React 會依賴 hook 呼叫的順序,而將 hook 放在迴圈、判斷語句中會影響呼叫的順序,可能導致狀態發生預期外的變化。
Only Call Hooks at the Top Level
Don’t call Hooks inside loops, conditions, or nested functions. Instead, always use Hooks at the top level of your React function, before any early returns. By following this rule, you ensure that Hooks are called in the same order each time a component renders. That’s what allows React to correctly preserve the state of Hooks between multiple
https://legacy.reactjs.org/docs/hooks-rules.html#explanationuseState
anduseEffect
calls. (If you’re curious, we’ll explain this in depth below.)
use 的優勢
假如不透過任何第三方套件在 Client component 中做 data fetching 以及處理 loading 狀態的話,這邊用最基本的 useState
和用 use
寫法來做個簡單對比。
useState
需要用到 isLoading
和 data
兩個狀態:
import React, { useState, useEffect } from "react";
export default function App() {
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState([]);
useEffect(() => {
setIsLoading(true);
fetch("https://jsonplaceholder.typicode.com/todos")
.then((response) => response.json())
.then((json) => setData(json))
.finally(() => setIsLoading(false));
}, []);
return (
<div>
{isLoading ? (
<p>Loading...</p>
) : (
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
)}
</div>
);
}
use + Suspense
但如果改用 use
hook 的話,由於 use
hook 讀取的是 Promise 的值,而 Suspense 會等待 Promise 解決再渲染組件,所以可以藉由 use
+ Suspense
來取代原先的 isLoading
和 data
兩個狀態。
import React, { use, Suspense } from "react";
const todosPromise = fetch(
"https://jsonplaceholder.typicode.com/todos"
).then((response) => response.json());
const App = () => {
const data = use(todosPromise);
return (
<Suspense fallback={<p>Loading...</p>}>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</Suspense>
);
};
export default App;
Suspense 會捕獲 Promise,在 Promise 解決之前先顯示 fallback 內容,當 Promise 解決後才會顯示組件內容,所以就不需要另外記錄 loading 狀態,直接用 Suspense 取代
isLoading
用 useState
來記錄 loading 和 data 需要 render 4 次,而用 use
+ Suspense
總共只需要 render 2 次,整整少了一半。
上面的例子還可以再將 Todos 細拆成 Client Component:
// Todos.js
"use client";
import { use } from "react";
const Todos = ({ todosPromise }) => {
const data = use(todosPromise);
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
};
export default Todos;
// App.js
import React, { Suspense } from "react";
import { fetchTodos } from "./lib";
import Todos from "./Todos";
const App = () => {
const todosPromise = fetchTodos();
return (
<Suspense fallback={<p>Loading...</p>}>
<Todos todosPromise={todosPromise} />
</Suspense>
);
};
export default App;
// lib.js
export const fetchTodos = () => {
return fetch("https://jsonplaceholder.typicode.com/todos").then((response) =>
response.json()
);
};
使用 use 的常見錯誤
死循環
use
會在獲取到數據後重新渲染組件,所以千萬不要直接在 use 裡面調用函數,否則就會死循環。
// ❌
const fetchTodos = () => {
fetch("https://jsonplaceholder.typicode.com/todos").then((response) =>
response.json()
);
}
const App = () => {
const data = use(fetchTodos());
return (
<Suspense fallback={<p>Loading...</p>}>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</Suspense>
);
};
// ✅
const fetchTodos = () => {
fetch("https://jsonplaceholder.typicode.com/todos").then((response) =>
response.json()
);
}
const todosPromise = fetchTodos();
const App = () => {
const data = use(todosPromise);
return (
<Suspense fallback={<p>Loading...</p>}>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</Suspense>
);
};
export default App;
async/await is not yet supported in Client Components
下面的例子會出現這個錯誤:
async/await is not yet supported in Client Components, only Server Components. This error is often caused by accidentally adding `'use client'` to a module that was originally written for the server.
const App = () => {
const todosPromise = fetch(
"https://jsonplaceholder.typicode.com/todos"
).then((response) => response.json());
const data = use(todosPromise);
return (
<Suspense fallback={<p>Loading...</p>}>
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
</Suspense>
);
};
這是因為目前 Client component 中並不支持直接使用 async/await 語法,建議的解決方式是將 獲取資料的組件 和 使用hook的組件 分開,以上面的例子來改就會變成:
// App.js
import Todos from './Todos';
const App = () => {
const todosPromise = fetch(
"https://jsonplaceholder.typicode.com/todos"
).then((response) => response.json());
return (
<Suspense fallback={<p>Loading...</p>}>
<Todos todosPromise={todosPromise} />
</Suspense>
);
};
export default App;
// Todos.js
"use client";
import { use } from "react";
const Todos = ({ todosPromise }) => {
const data = use(todosPromise);
return (
<ul>
{data.map((item) => (
<li key={item.id}>{item.title}</li>
))}
</ul>
);
};
export default Todos;