https://zenn.dev/kazuma1989/articles/a30ba6e29b5b4c
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
で参照の同一性を保っています。