Reduxを使ってのローディング表現をSuspenseでなんとかしたい。
そろそろSuspenseと向き合う
React 18のメジャーアップデートが控えている中まだSuspenseを理解していないかつ
これはSuspenseの出番ではないかとなってきた場面に出くわしたので挑戦してみた。
という学習記録です。
Redux Toolkitの現状の使い方について
現在勤めている会社では、Redux Toolkitを使用しています。
使い方としては主に以下になります。
- SliceでDomain+loadingのstateを定義
- createAsyncThunkを使用して、fetch非同期処理を行いデータ取得する。
- extraReducers内でpromiseの状態に合わせた処理
- reducersで状態を変更する。
Loading管理ツールなっていないか
現在2つほど、SPA開発を行なってきたのですが、保持している状態の使い方を洗い出します
- Userのログイン情報の保持、ページ遷移時などにAuthチェックを行い遷移するか決める
- データフェッチしたデータを保持、基本的に一つのページでしか使われない
- データフェッチする際のloading状態
ほぼcreateAsyncThunk分、loading状態をすることに違和感がある。
以下が例です。
const pokemonSlice = createSlice({
name: 'pokemon',
initialState: {
data: [],
isLoading: false,
}
~~ 省略 ~~
builder.addCase(fetchAsyncGetPokemon.pending, (state) => {
state.isLoading = true
})
builder.addCase(fetchAsyncGetPokemon.rejected, (state) => {
state.isLoading = false
})
builder.addCase(fetchAsyncGetPokemon.fulfilled,
(state, action: PayloadAction<SliceResponseType<GetShopResponseType>>) => {
if (action.payload.status === 200) {
state.isLoading = false
~~ データ系 ~~
}
})
Suspenseでpending状態にローディングを実装できそう。
Suspenseとは
React 16.6 で、コードのロードを「待機」して宣言的にロード中状態(スピナーのようなもの)を指定することができる <Suspense> コンポーネントが追加されました。引用: React ドキュメント https://ja.reactjs.org/docs/concurrent-mode-suspense.html
さっそく試す
ReactのDocを見ながら実装していく。
データ取得実装
今回はpokemonAPIを使ってデモを作成する。
// ここで受け取るpromiseというのは、
// codeでいうとconst result = getPokemonFetch()が入っている。
const wrapPromise = (promise) => {
let status = 'pending';
let result;
const suspender = promise.then(
(r) => {
status = 'fulfilled';
result = r;
},
(e) => {
status = 'rejected';
result = e;
});
// 最初はsuspender(promiseオブジェクト)をthrowする。
const read = () => {
if (status === 'pending') {
throw suspender;
} else if (status === 'rejected') {
throw result;
} else {
return result;
}
}
return { read };
}
const getPokemonFetch = async () => {
const url = 'https://pokeapi.co/api/v2/pokemon';
const res = await fetch(url);
return res.json();
}
export const getPokemon = () => {
const result = getPokemonFetch();
// wrapPromise関数でfetchでreturnされるpromise(変数名: result)を引数で設定
return wrapPromise(result);
}
getPokemonFetch
についてはfetchでAPIを呼び出しているので説明は割愛します
wrapPromise
関数で
- pending時はpromiseをthrowさせる
- fulfilled時にresponseを返却する
- rejected時のresponseをthrowする
といったことを行っている
getPokemon
呼び出す
使用コンポーネントからimport React from 'react'
import { getPokemon } from '../../utils/getPokemon'
const pokemon = getPokemon()
export default function List() {
const data = pokemon.read()
const pokemonResult = data.results
return (
<div>
{
pokemonResult.map((pokemon) => {
return (
<div key={pokemon.name}>
{pokemon.name}
</div>
)
})
}
</div>
)
}
Susupenseを宣言
Route Domを使用し、List
コンポーネントを表示する設定を行なっています。
Routes
の親にSuspenseを宣言しています。
Suspenseがサスペンドを受け取り、
まだレンダリングできないのかと理解し、fallback propsを表示しする
解決したらレンダリングするようになっている。便利
import React, { lazy, Suspense } from 'react'
import { Route, Routes } from 'react-router'
const Details = lazy(() => import('./components/pages/Details'))
const Home = lazy(() => import('./components/pages/Home'))
const List = lazy(() => import('./components/pages/List'))
export const Router = () => {
return (
<div>
<Suspense fallback={<div>nowloading...</div>}>
<Routes>
<Route path="/" element={<Home />} />
<Route path="details" element={<Details />} />
<Route path="pokemon" element={<List />} />
</Routes>
</Suspense>
</div>
)
lazy関数について
今回の例で、すでにlazy関数が使用されていますが
lazy関数で指定されたコンポーネントたちは、レンダーされる時にバンドルをロードするといった流れで描画までもっていくことができます。
なのでロードされるまで、Promiseを返すことができます。
また今回の学習においては、getPokemonをcomponentの外部で変数定義しています。
この場合、どのコンポーネントを表示する時でも呼び出されてしまいます。
バンドルを行なっているので、最終的に出力されるファイルは一つになるので呼び出されます。
動的importかつlazyを使用して動的に読み込むようにさせています。
動的な import() 構文
だけでなんとかなりますね
import React from 'react'
import { getPokemon } from '../../utils/getPokemon'
const pokemon = getPokemon() // これ
export default function List() {
const data = pokemon.read()
その他にも実装方法はあった
Next.jsではReact 18のbetaを試すことのできます!神
Suspenseの使用についてコードが残されてあったので紹介します。
const cache = {}
export default function useData(key, fetcher) {
if (!cache[key]) {
let data
let promise
cache[key] = () => {
if (data !== undefined) return data
if (!promise) promise = fetcher().then((r) => (data = r))
throw promise
}
}
return cache[key]()
}
const cacheに対してthrow promise
結果
が入るようになっていますね。
keyを持たせることによって、1度のfetchで済むようになっていますね。
勉強になります。
リポジトリは以下
おわり
以上がSuspense試した記録でした。
タイトルにReduxと書かれているのにも関わらず、序盤でしかReduxについて触れませんでした。
ただReduxでの対応も動いているようなので情報を追っていきたいと思っています。
まだ本質を掴めていない感があるので引き続き試していきたい。
今回の内容は以下のリポジトリに記載されています
参考
Suspense
React.lazy
Next.js React 18 beta demo
React-Redux Roadmap: version 8.x, React 18, and TypeScript
Reactの次期機能のSuspenseが凄くって、非同期処理がどんどん簡単になってた!
React 18に備えるにはどうすればいいの? 5分で理解する
Discussion