React 19で追加されたuseを使って宣言的に、責務を分離しながら、簡潔に、非同期データを利用
はじめに
SREホールディングス株式会社でフロントエンドエンジニアをしている橋本です。
React 19が12/5にリリースされましたね。
React 19で追加された新機能のuse
がなかなか便利そうかつ面白いヤツだったので、試してみました。
use
React 19で追加された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