◆ React の Suspense を使おうと思う
◆ use は experimental なので 18 だと使えない

最近は React を使うときに 18 がメインになってきましたが 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 の方がいいですね