React 19で追加されたuseを使って宣言的に、責務を分離しながら、簡潔に、非同期データを利用
はじめに
SREホールディングス株式会社でフロントエンドエンジニアをしている橋本です。
React 19が12/5にリリースされましたね。
React 19で追加された新機能のuseがなかなか便利そうかつ面白いヤツだったので、試してみました。
React 19で追加されたuse
useStateやuseEffectなどのフックの子供みたいな見た目をしているuseですが、似た性質はあるもののフックではありません。ReactのAPIリファレンスでもフックではなく、その他のAPIとして説明されています。
フックと似ている点
- 関数コンポーネントもしくはフック内からしか呼び出せない。
フックと異なる点
- ifブロック内など、トップレベルではない場所から呼び出せる
useの使い道
useには「Contextから値を読み出す」と「Promiseから値を読み出す」という二つの役割があります。
Contextから値を読み出す
上位のコンポーネントからUIツリーの深いところまで値を渡す際に便利なContextは従来は以下のように使われていました。(ソースコードはAPIリファレンスより引用)
利用側のButtonコンポーネントがThemeContext.Providerの内部にあれば、どこにいてもプロバイダー側が提供する値を読み出すことができます。
利用側
import { useContext } from 'react';
function Button() {
const theme = useContext(ThemeContext);
// ...
プロバイダー側
function MyPage() {
return (
<ThemeContext.Provider value="dark">
<Form />
</ThemeContext.Provider>
);
}
function Form() {
// ... renders buttons inside ...
}
useを使っても、従来のuseContextと同様の書き方をすることができます。ただし前述したようにuseはifブロック以下にも書くことができるので、useContextよりこちらを使うことが推奨されています。(useでコンテクストを読み取る)
余談ですが、React 19からプロバイダー側にも新しい書き方が追加され、<Context.Provider>ではなく<Context>と簡潔に書けるようになりました。将来的には<Context.Provider>は非推奨になるようです。(<Context> がプロバイダに)
まとめるとReact 19からContextは以下のように書けるようになりました。
利用側
import { useContext } from 'react';
function Button() {
// useを使用
const theme = use(ThemeContext);
// ...
プロバイダー側
function MyPage() {
return (
// providerを省略
<ThemeContext value="dark">
<Form />
</ThemeContext>
);
}
function Form() {
// ... renders buttons inside ...
}
Promiseから値を読み出す
こちらの使い方が本記事のメインターゲットです。
useは引数にPromiseを受け取り、同期的に(awaitもthenも使わずに)値を読み出すことができます!ただしUIツリーの上の階層においてuseを含むコンポーネントを<Suspense>で囲む必要があります。
import { use } from "react";
type GreetWithUseProps = {
textPromise: Promise<string>;
};
export const GreetWithUse = ({ textPromise }: GreetWithUseProps) => {
// Promiseの解決済みの値を利用
const text = use(textPromise);
return <div>{text}</div>;
};
export default function App() {
const greetingPromise: Promise<string> = fetchGreeting();
return(
// Promiseが解決済みでない場合はfallbackの<LoadingMessage />が表示される
<Suspense fallback={<LoadingMessage />}>
<GreetWithUse textPromise={greetingPromise} />
</Suspense>
);
}
このようにuseを使用することで、Promiseが解決済みの場合は値を同期的に使用することができ、解決していない場合は<Suspense>が勝手に代わりのコンポーネントを表示してくれます。
そのため非同期的に取得されるデータを利用するコンポーネントでも、非同期処理の状態を全く気にせずに書くことができます!
従来のパターン(useEffect)
ここでコードの簡潔を比べるために従来のパターンと比較してみます。
従来、ReactでTanstackQueryなどのライブラリを使わずにデータを取得する時にはuseEffectを使う方法が一般的でした。(Approach 1: Fetch-on-Render (not using Suspense))
export const Greet = () => {
const [greeting, setGreeting] = useState<string | undefined>();
useEffect(() => {
fetchGreeting().then(g => setGreeting(g));
}, []);
if (!greeting) {
return(<LoadingMessage />)
}
return(<div>{text}</div>)
}
export default function App() {
return(<Greet />);
}
Reactではレンダーの最中では非同期関数を待つことができないため、フェッチのための非同期関数をuseEffectの中に避難しています。
しかしこの書き方では<Greet />コンポーネントがfetchGreetingに依存してしまっているため、他のAPIが増えた時に再利用できません。フェッチ関数は上のコンポーネントへ移動させ、<Greet />コンポーネントには表示のみに専念してもらいましょう。
export const Greet = ({ text }: GreetingPrpos) => {
return(<div>{text}</div>)
}
export default function App() {
const [greeting, setGreeting] = useState<string | undefined>();
useEffect(() => {
fetchGreeting().then(g => setGreeting(g));
}, []);
if (!greeting) {
return(<LoadingMessage />);
}
return(
<Greet text={greeting} />
);
}
このようになりました。
例のようにAPI一つ、コンポーネント一つの場合はこのように書いても人間に扱えるボリュームですが、一つの画面に複数のAPIとそれぞれに影響されるコンポーネントがある場合はどうでしょうか。APIそれぞれにstate、LoadingMessageが必要となり、ソースコードが肥大化していきます。
さらにもう一つ問題を挙げると、useEffectを使った書き方では一度ブラウザの描画が完了してからフェッチが実行され、取得されたデータを使って再びブラウザの描画を行うという振る舞いになってしまいます。(これをFetch-on-Renderパターンと言います)
これを回避するにはReact 18で正式リリースされていた<Suspense />を使う方法(Approach 3: Render-as-You-Fetch (using Suspense))がありましたが、use導入以前は対応フレームワークでの利用以外は推奨されていませんでした。
useが使えることの嬉しさ
ここでuseを使ったソースコードを再掲します。
// importと型定義省略
export const GreetWithUse = ({ textPromise }: GreetWithUseProps) => {
// Promiseの解決済みの値を利用
const text = use(textPromise);
return <div>{text}</div>;
};
export default function App() {
const greetingPromise: Promise<string> = fetchGreeting();
return(
// Promiseが解決済みでない場合はfallbackの<LoadingMessage />が表示される
<Suspense fallback={<LoadingMessage />}>
<GreetWithUse textPromise={greetingPromise} />
</Suspense>
);
}
useを使うと非同期処理の状態を気にすることなく宣言的に書くことができています。
加えてuseEffectから脱却したことにより、データフェッチの関数をコンポーネントレンダリングより前に実行することが可能になりました。それによりレンダリングとフェッチを同時に行うことが可能になり、より効率的にデータフェッチとレンダリングを行うことができます。(Render-as-You-Fetchパターンと言います)
複数のスピナーつきUIのサンプル
さらにuseの威力を感じるため、1つの画面で3つの非同期関数からデータを取得するサンプルを書きました。
データが取得されるタイミングをランダムにしているので、画面の中でロードが完了したものから逐次データが表示されていることがわかると思います。
取得されるデータは「SRE」「Holdings」「株式会社」のいずれかです。
ぜひ弊社「SRE Holdings 株式会社」を揃えてSNSに投稿してくださいね(?)
まとめ
React 19で正式リリースされたuseについて紹介しました。
useを使うことによってContextの読み出しがifブロック内などのトップレベルでない箇所で行えるようになりました。コンポーネント全体でContextの値を使用するのであればuseContextを使用する場合とあまり変わらないと思いますが、とある条件の時に1箇所だけで使用するような値であれば、使用箇所の直前にContextの宣言が書けるのでコードが読みやすくなると思います。
またPromiseの値を同期的に読み出すことができ、Reactの機能だけで<Suspense />が扱えるようになりました。非同期通信の状態を意識せずに、責務を分離しながら記述量も削減できて、大変読みやすいコードが書けるようになっています。実際の開発では、キャッシュなどでまだまだライブラリを手放すことはないと思いますが、Reactの機能だけで対応できる範囲が広がっているのも嬉しいです。
Discussion