🔥

Reduxを使ってのローディング表現をSuspenseでなんとかしたい。

2021/12/08に公開

そろそろ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()

React.lazy

その他にも実装方法はあった

Next.jsではReact 18のbetaを試すことのできます!神
https://nextjs.org/docs/advanced-features/react-18

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で済むようになっていますね。
勉強になります。

リポジトリは以下
https://github.com/vercel/next-rsc-demo

おわり

以上がSuspense試した記録でした。
タイトルにReduxと書かれているのにも関わらず、序盤でしかReduxについて触れませんでした。

ただReduxでの対応も動いているようなので情報を追っていきたいと思っています。
https://github.com/reduxjs/react-redux/issues/1740

まだ本質を掴めていない感があるので引き続き試していきたい。

今回の内容は以下のリポジトリに記載されています
https://github.com/nakamotoyuto/Suspense-Practice

参考

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