React 18 的新 hook `use` 是什麼?有何優勢?

React 18 的新 hook `use` 是什麼?有何優勢?

由於 use 是比較新的 hook 目前可參考資料不多,文章中有些內容可能不是特別準確或者存在錯誤,若有任何錯誤歡迎在評論指出,我會馬上修正。

什麼是 use?

use 是 React 18 新出的一個 hook,目前只能在 canaryexperimental 渠道中使用。

這個 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 版本

npm update react@canary react-dom@canary
https://react.dev/community/versioning-policy#all-release-channels

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 useState and useEffect calls. (If you’re curious, we’ll explain this in depth below.)

https://legacy.reactjs.org/docs/hooks-rules.html#explanation

use 的優勢

假如不透過任何第三方套件在 Client component 中做 data fetching 以及處理 loading 狀態的話,這邊用最基本的 useState 和用 use 寫法來做個簡單對比。

useState

需要用到 isLoadingdata 兩個狀態:

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 來取代原先的 isLoadingdata 兩個狀態。

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 次,整整少了一半。

react usestate
react use

上面的例子還可以再將 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;

0000-first-class-support-for-promises

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *