データフェッチはuseEffectの出番じゃないなら、結局何を使えばいいんだ
ショートアンサー
React 18 からのフックである、useSyncExternalStore
を使えばいいようです。
※ useEffect
がまったくだめだというわけではありません。
※ クライアントサイドレンダリングのみを考えています。サーバーサイドレンダリングを考慮すると違った答えになるかもしれません。
サンプルコード
次のような useData
フックを作ってみます。
JSON API の GET レスポンスを返すシンプルなものです。
実験をしやすいように、リクエスト URL を変えるボタンを置いてあります。
import { useEffect, useState } from "react"
export function SearchResults() {
const [id, setID] = useState(1)
const todo = useData(`https://jsonplaceholder.typicode.com/todos/${id}`)
return (
<div>
<button
type="button"
onClick={() => {
setID((id) => id + 1)
}}
>
Increment ID (current: {id})
</button>
<pre>{JSON.stringify(todo, null, 2)}</pre>
</div>
)
}
function useData<T>(url: string): T | undefined {
// ...
}
useEffect
版
次のリンクから持ってきたものです。
fetch の abort をしなくていいんだろうかと思いつつ、サンプルそのままです。
型や変数名は変えています。
function useData<T>(url: string): T | undefined {
const [data, setData] = useState<T>()
useEffect(() => {
let ignore = false
fetch(url)
.then((res) => res.json())
.then((data) => {
if (!ignore) {
setData(data)
}
})
return () => {
ignore = true
}
}, [url])
return data
}
リンク先の記事では、この簡易なサンプルを紹介しつつも、Next.js などのフレームワークが用意した仕組みを使うとよいと述べています(React はフレームワークではないので)。
なので、「データフェッチにおいて effect は必要(必須)ではない」ようです。
useSyncExternalStore
版
独自に考えたものです。
function useData<T>(url: string): T | undefined {
const data$ = useRef<T>()
const subscribe = useCallback(
(onStoreChange: () => void): (() => void) => {
const controller = new AbortController()
fetch(url, { signal: controller.signal })
.then((res) => res.json())
.then((data) => {
data$.current = data
onStoreChange()
})
return () => {
controller.abort()
}
},
[url]
)
return useSyncExternalStore(subscribe, () => data$.current)
}
useSyncExternalStore
が相手にする external store は、ここでは Promise (fetch(url).then((res) => res.json())
) です。
第 1 引数 subscribe
は、external store の変化を通知するコールバック onStoreChange
を登録する関数です。
ここでは Promise の解決したときが通知タイミングなので、then
内でコールバックを呼んでいます。
subscribe
の参照が変わると再レンダリングが生じるため(ここでは無限ループにつながります)、useCallback
で参照の同一性を保っています。
第 2 引数 getSnapshot
は、external store のそのときの値を返す関数です。
ここでは、Promise 解決後の値を Promise から同期的に取得できないため、ref を使ってなんとかしています。
説明(思考ダンプ)
useEffect
の出番じゃない」って誰が言ってるんだ
「データフェッチは Dan Abramov さんです。
まず、彼は「useEffect
は React ツリー外と props/state をシンクロする道具だ」と述べています。
useEffect
lets you synchronize things outside of the React tree according to our props and state.
API リクエストを 1 回だけ送りたいとか、パラメーターが変わったのに反応して送りたいとか、そういう使い方をすべきではない。
「props/state と API レスポンスとをシンクロする」と考えなさいということですね。
しかしながら、後半では 「データフェッチは正確にはシンクロ問題ではない」 と述べています。
So far,
useEffect
is most commonly used for data fetching. But data fetching isn’t exactly a synchronization problem.
「データフェッチは useEffect
の出番じゃない」ということです。
次の記事でも、同じようなことを述べています。
両者とも「たしかに簡単だから使ってもいいけれど、別の手法がベターだ」というスタンスのようです。
「データフェッチは正確にはシンクロ問題ではない」に納得する
キーは、シンクロ問題とは React ツリー外 と props/state を相手にしているということです。
useEffect
版のサンプルコードをよく見ると、useData
フック内で url
(state である id
から導出したものなので、state です)とシンクロしているのは、「React ツリー外のもの」ではありません。
React ツリーのど真ん中の state である、data
です。
function useData<T>(url: string): T | undefined {
const [data, setData] = useState<T>()
useEffect(() => {
let ignore = false
fetch(url)
.then((res) => res.json())
.then((data) => {
if (!ignore) {
setData(data)
}
})
return () => {
ignore = true
}
}, [url])
return data
}
このことが、「正確には」シンクロ問題ではないと言わしめていると考えました。
じゃあデータフェッチは何者なんだ
External store なんだと思います。
たとえば、以下の useSyncExternalStore
のサンプルコードでは、navigator.onLine
API を external store として扱っています。
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
function useOnlineStatus() {
// ✅ Good: Subscribing to an external store with a built-in Hook
return useSyncExternalStore(
subscribe, // React won't resubscribe for as long as you pass the same function
() => navigator.onLine, // How to get the value on the client
() => true // How to get the value on the server
);
}
function ChatIndicator() {
const isOnline = useOnlineStatus();
// ...
}
External な理由は、React の状態管理とは独立した状態システムの上にあるからでしょう。
同様に、HTMLMediaElement や Intersection Observer API も external store になるでしょう。
データフェッチの結果も、ネットワーク I/O という独立した状態システムの上にあると考えられそうです。
つまり、external store と考えて、差し支えなさそうです。
データフェッチを XMLHttpRequest で行ったとすると、addEventListener でレスポンスを取得することになり形の上でも navigator.onLine
API と同じになるため、データフェッチはやはり external store でしょう。
useSyncExternalStore
でも書き味大して変わらないな
とはいえ そうです。
結局は、SWR や React Query を足したり、もしくは Next.js などのフレームワークに従ったりすべきです。
useEffect
を使う以外ないのではないか
「このページを訪れたら」は そう思っています。
ページビューを Google Analytics に送るような場合ですね。
次の記事にサンプルコードがあります。
ただし、これはユーザーアクションを契機にした処理であって、シンクロ問題ではありません。
シンクロ問題なのであれば、効率はさておき何度同じページビューを送っていいはずですが、そうではないからです。
現実に useEffect
を使う以外ないのは、React にそういう機能がまだ or 意図的にないからだと思います。
公式に言及があったかわかりませんが(知っていれば教えてください🙏)、個人的には意図的なんだと思います。
「ページを訪れたら」はルーティングとともに起きる話で、ルーティングは Next.js などのフレームワークや React Router などのサードパーティーに任されていることから、React 本体としては関与しない箇所なのではと。
まとめ
React でのデータフェッチは、useEffect
でもいいけど、専用に作られた useSyncExternalStore
を使うのがいい。
もっといいのは、データフェッチ用ライブラリーなどを使うこと。
useEffect
は便利だしなくせない。
Discussion