React useを触ってみた
まえがき
エンジニアの恒川です。
私は現在Next.js App Routerを用いたアプリケーション開発をしています。Next.js 15からReact 19の使用が始まることを受けて、Reactのuse APIでどんなことができるのか実際に触ってみました。
use
import { use } from 'react';
function MessageComponent({ messagePromise }) {
const message = use(messagePromise);
const theme = use(ThemeContext);
// ...
useはPromiseまたはContextを渡すとそれらを解決してくれる(値を取り出してくれる)ようなAPIです。
Contextに対する使用はuseContextとほぼ役割が被っていますが、useContextと異なりifやfor内部で使用できることからuseの使用が推奨されています。
以下ではNext.js 15 App Routerと組み合わせたPromiseに対する使用例について紹介していきます。
サンプルコード
2つのコンポーネントがそれぞれデータフェッチを行うような簡易な例を用意しました。
TodoコンポーネントとMessagesコンポーネントはそれぞれサーバーコンポーネントであり、コンポーネントが描画に必要なデータをフェッチします。
サンプルコード
async function Page() {
return (
<>
<Suspense fallback={<div>loading...</div>}>
<Todo />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<Messages />
</Suspense>
</>
);
}
export const Todo = async () => {
const todo = await fetchTodo();
return <div>{todo}</div>;
};
export const Messages = async () => {
const messages = await fetchMessages();
return (
<>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</>
);
};
お題)Messagesをクライアントコンポーネントにする
上記の例題についてMessagesコンポーネントをクライアントコンポーネントにすることを考えます。useParamsなどのフックを使用したい場合などはクライアントコンポーネントである必要がありますが、クライアントコンポーネントはasync関数にできません。
"use client";
export const Messages = async () => {
const messages = await fetchMessages();
return (
<>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</>
);
};
async/await is not yet supported in Client Components, only Server Components.
というエラーが出てしまいます。
方法1. 親のサーバーコンポーネントでフェッチする
親のサーバーコンポーネントでフェッチを行い、データをMessagesコンポーネントに渡すことを思いつくかもしれません。Fetch-then-Renderパターンと呼ばれ、レンダリング開始前にデータフェッチが完了するようなパターンです。
async function Page() {
const messages = await fetchMessages();
return (
<>
<Suspense fallback={<div>loading...</div>}>
<Todo />
</Suspense>
<Messages messages={messages} />
</>
);
}
"use client";
export const Messages = ({ messages }: { messages: string[] }) => {
return (
<>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</>
);
};
この方法ではfetchMessages()
を待っている間、Pageコンポーネントのレンダリングがブロックされてしまう点が気になります。また、fetchMessages()
が完了してからTodoコンポーネントのレンダリングが開始されるため、fetchMessages()
→fetchTodo()
の順に直列で待ちが発生してしまいます。
Fetch-then-Render
方法2. クライアントコンポーネントでフェッチする
Messagesコンポーネントをasync関数にできませんが、useEffectとuseStateを用いてフェッチ可能です。これはFetch-on-Renderと呼ばれるパターンで、読者の皆様もよく目にするパターンかと思います。
async function Page() {
return (
<>
<Suspense fallback={<div>loading...</div>}>
<Todo />
</Suspense>
<Messages />
</>
);
}
"use client";
export const Messages = () => {
const [messages, setMessages] = useState<string[] | null>(null);
useEffect(() => {
fetchMessages().then((messages) => {
setMessages(messages);
});
}, []);
if (!messages) {
return <div>loading...</div>;
}
return (
<>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</>
);
};
この方法ではMessagesコンポーネントの処理が複雑になってしまう・Suspenseを使ったローディング状態の管理ができないといったデメリットがあると考えています。ただし、SWRなどのデータフェッチライブラリではSuspenseのためのオプションが用意されており、それらを使用してシンプルな処理でSuspenseを使うことも可能そうです。
Fetch-on-Render
方法3. クライアントコンポーネントでuseを使う
最後にMessagesコンポーネントでuseを使用する方法を紹介します。
async function Page() {
const messagesPromise = fetchMessages();
return (
<>
<Suspense fallback={<div>loading...</div>}>
<Todo />
</Suspense>
<Suspense fallback={<div>loading...</div>}>
<Messages messagesPromise={messagesPromise} />
</Suspense>
</>
);
}
"use client";
export const Messages = ({
messagesPromise,
}: { messagesPromise: Promise<string[]> }) => {
const messages = use(messagesPromise);
return (
<>
<ul>
{messages.map((message, i) => (
<li key={i}>{message}</li>
))}
</ul>
</>
);
};
ポイントはPageコンポーネントでfetchMessages()
の完了を待たず、PromiseをMessagesコンポーネントに渡している点です。これによりPageコンポーネントのレンダリングはブロックされません。渡されたPromiseはMessagesコンポーネント内でuseを使って解決されます。
処理はシンプルなまま、Suspenseを使ったローディング状態の管理もできていてGoodですね。
Render-as-you-Fetch
まとめ
Next.js App Routerを使った例題に対して、React useを用いたPromiseの解決について紹介しました。useを使うとクライアントコンポーネントを疑似的にasync関数のように扱うことができるため、書いていてなかなか気分がよいと思いました。
この記事を書いた人
恒川 雄太郎
2021年キャリア入社
風邪をひきやすい季節です。ひいたと思ったら「ニンニクたっぷりペペロンチーノ」「高めの風邪薬」「高めの栄養ドリンク」を体に入れると気分がよくなると思います。
Discussion