そろそろ Suspense 使ってみようか
- カテゴリ:
- JavaScript
- コメント数:
- Comments: 0
◆ React の Suspense を使おうと思う
◆ use は experimental なので 18 だと使えない
◆ use は experimental なので 18 だと使えない
最近は React を使うときに 18 がメインになってきましたが Suspense は全く使ったことがないです
出てきた頃の紹介で 基本はライブラリが使ってあまり直接使うものじゃないみたいなのを見たので特に使おうとも思ってなかったです
ただ普通に使ってる例をみかけたこともあって使ってみようかなという状態です
https://react.dev/reference/react/Suspense
Next.js みたいなライブラリを使ったり lazy や use を使う必要があるようなことが書かれています
lazy はコンポーネント自体のロードを遅延させるものなので use が必要になりそうです
でも use はまだ experimental ステータスで canary 版のみです
18 だと use がエクスポートされてないようです
unstable プレフィックス付きでもなさそうです
ドキュメントのサイトの例を見てみると use が独自に実装されていました
status や value などのプロパティはこの中でしか使わないので別の名前にしても大丈夫でした
use と Suspense を使った例はこんな感じです
これで 最初は fallback が表示されていて 1 秒後に OK が表示されるようになります
ところで promise がコンポーネントの外にあるところが気になります
直接 use にその場で作った Promise を渡す場合 Promise が解決されたときの再レンダリングでまた Promise が作られ throw されます
無限に再レンダリングされることになります
回避しようと useMemo や useRef を使ってみましたが効果はなかったです
メモや ref に Promise を保存しようとしてもそのレンダリングが throw されて中断されるので保存できないです
結果毎回 Promise を作ることになって無限に再レンダリングです
ドキュメントの例だと Promise を作る処理が fetch になっていて URL の一致でキャッシュしてるので問題ないという感じでした
何らかのキャッシュ機能を入れて Promise が使い回されるようしないとダメそうです
みたいな感じで state を更新します
これでもよかったのですが Promise をできるだけそのまま使いたいなと別の方法でこんなこともしてみたりです
Await コンポーネント内で Promise を処理してます
Suspense と比べると Suspense だとその内側で非同期処理を行って準備中なら Promise を throw することで Suspense コンポーネントに伝えますが この Await だと Await より外側で Promise を作って渡す必要があります
Await を使う外側で準備用の非同期処理を知る必要があるので Suspense の方がいいですね
出てきた頃の紹介で 基本はライブラリが使ってあまり直接使うものじゃないみたいなのを見たので特に使おうとも思ってなかったです
ただ普通に使ってる例をみかけたこともあって使ってみようかなという状態です
Suspense
とりあえずドキュメントを見てみましたhttps://react.dev/reference/react/Suspense
Next.js みたいなライブラリを使ったり lazy や use を使う必要があるようなことが書かれています
lazy はコンポーネント自体のロードを遅延させるものなので use が必要になりそうです
でも use はまだ experimental ステータスで canary 版のみです
18 だと use がエクスポートされてないようです
unstable プレフィックス付きでもなさそうです
ドキュメントのサイトの例を見てみると use が独自に実装されていました
function use(promise) {
if (promise.status === 'fulfilled') {
return promise.value;
} else if (promise.status === 'rejected') {
throw promise.reason;
} else if (promise.status === 'pending') {
throw promise;
} else {
promise.status = 'pending';
promise.then(
(result) => {
promise.status = 'fulfilled';
promise.value = result;
},
(reason) => {
promise.status = 'rejected';
promise.reason = reason;
}
);
throw promise;
}
}
status や value などのプロパティはこの中でしか使わないので別の名前にしても大丈夫でした
use と Suspense を使った例はこんな感じです
const App = () => {
return (
<div>
<h1>H1</h1>
<Suspense fallback={<div>fallback</div>}>
<Component1 />
</Suspense>
</div>
)
}
let promise = null
const Component1 = () => {
if (!promise) {
promise = new Promise((resolve) => setTimeout(resolve, 1000, "OK"))
}
const result = use(promise)
return <div>{result}</div>
}
これで 最初は fallback が表示されていて 1 秒後に OK が表示されるようになります
ところで promise がコンポーネントの外にあるところが気になります
直接 use にその場で作った Promise を渡す場合 Promise が解決されたときの再レンダリングでまた Promise が作られ throw されます
無限に再レンダリングされることになります
回避しようと useMemo や useRef を使ってみましたが効果はなかったです
const result = use(
useMemo(() => {
return new Promise((resolve) => setTimeout(resolve, 1000, "OK"))
}, [])
)
const ref = useRef()
if (!ref.current) {
ref.current = new Promise((resolve) => setTimeout(resolve, 1000, "OK"))
}
const result = use(ref.current)
メモや ref に Promise を保存しようとしてもそのレンダリングが throw されて中断されるので保存できないです
結果毎回 Promise を作ることになって無限に再レンダリングです
ドキュメントの例だと Promise を作る処理が fetch になっていて URL の一致でキャッシュしてるので問題ないという感じでした
何らかのキャッシュ機能を入れて Promise が使い回されるようしないとダメそうです
今の方法
Suspense を使っていない今の方法は 基本的な state で管理する方法ですfetchApi("user").then(user => setUser(user))
みたいな感じで state を更新します
これでもよかったのですが Promise をできるだけそのまま使いたいなと別の方法でこんなこともしてみたりです
const user = useMemo(() => {
return fetchApi("user")
})
const categories = useMemo(() => {
return fetchApi("categories")
})
return (
<div>
<div>
<Await promise={user}>
{(user) => (
<div>{user.name}</div>
)}
</Await>
</div>
<div>
<Await promise={categories}>
{(categories) => (
<ul>{categories.map((c, i) => <li key={i}>{c.name}</li>)}</ul>
)}
</Await>
</div>
</div>
)
Await コンポーネント内で Promise を処理してます
const Await = ({ promise, children }) => {
const [state, setState] = useState()
useEffect(() => {
let skip = false
promise.then(value => {
if (skip) return
setState(value)
})
return () => {
skip = true
}
}, [promise])
if (!state) return null
return children(state)
}
Suspense と比べると Suspense だとその内側で非同期処理を行って準備中なら Promise を throw することで Suspense コンポーネントに伝えますが この Await だと Await より外側で Promise を作って渡す必要があります
Await を使う外側で準備用の非同期処理を知る必要があるので Suspense の方がいいですね