🧐

Expo×Supabase×Redux Toolkitを使った状態管理実装ガイド

に公開

はじめに

近年、SupabaseはPostgreSQLをベースとしたバックエンドサービスとして急速に注目を集めています。また、Expoを利用したReact Native開発では、リアルタイムのCRUD操作や認証機能が充実しているため、非常に使いやすい環境が整っています。
一方、Reactのローカル状態管理(useStateやuseContext)だけでは、コンポーネント間での状態連携が煩雑になりがちです。そこで、今回はRedux Toolkitを使用してグローバルな状態管理を実現する方法をご紹介します。具体的には、Supabaseのデータ取得・更新をRedux Toolkitの非同期アクションでラップし、その結果をグローバルなストアで管理する流れを解説します。

環境設定

Expoプロジェクトの初期化

まずはExpo CLIを使って新規プロジェクトを作成します。

npx create-expo-app my-expo-app
cd my-expo-app

Supabaseの設定

Supabaseの公式クライアントライブラリを利用して初期化します。
詳しくは公式のサイトの手順に従ってください。

https://supabase.com/docs/guides/getting-started/tutorials/with-expo-react-native?queryGroups=auth-store&auth-store=secure-store

Redux Toolkitの導入

Redux Toolkitをインストールします。

npm install @reduxjs/toolkit react-redux

Redux ToolkitでSupabase操作をラップする

Supabaseからのデータ取得・更新を、Redux Toolkitの非同期アクションとして定義し、グローバルなストアで管理します。

非同期アクションの定義

createAsyncThunkを利用して、Supabaseからユーザーデータを取得する例を示します。

// features/users/userSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import { supabase } from '../../supabaseClient';

// Supabaseからユーザーを取得する非同期関数
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
  const { data, error } = await supabase.from('users').select('*');
  if (error) throw error;
  return data;
});

Sliceの作成

次に、非同期アクションの結果をハンドルするSliceを定義します。

// features/users/userSlice.js
const userSlice = createSlice({
  name: 'users',
  initialState: {
    users: [],
    status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null,
  },
  reducers: {
    // 必要に応じて同期処理のreducersも定義
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded';
        state.users = action.payload;
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed';
        state.error = action.error.message;
      });
  },
});

export default userSlice.reducer;

コンポーネントでの利用例

Reactコンポーネント内で、useDispatchuseSelectorを利用して、非同期アクションを発火し、状態を取得します。

// components/UserList.jsx
import { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchUsers } from '../features/users/userSlice';

const UserList = () => {
  const dispatch = useDispatch();
  const { users, status, error } = useSelector((state) => state.users);

  useEffect(() => {
    if (status === 'idle') {
      dispatch(fetchUsers());
    }
  }, [status, dispatch]);

  if (status === 'loading') return <div>Loading...</div>;
  if (status === 'failed') return <div>Error: {error}</div>;

  return (
    <div>
      <h2>User List</h2>
      <ul>
        {users.map((user) => (
          <li key={user.id}>
            {user.name} ({user.email})
          </li>
        ))}
      </ul>
    </div>
  );
};

export default UserList;

メリット・デメリット

メリット

  • 一元管理が容易
    Redux Toolkitで非同期処理を統一することで、SupabaseのCRUD操作とUIの状態管理を一箇所で管理できます。

  • 非同期処理の標準化
    createAsyncThunkを利用することで、非同期処理の成功・失敗やローディング状態を簡単に管理できるため、エラーハンドリングや状態遷移が明確に把握できます。

  • 再利用性と拡張性
    一度定義したアクションやSliceは、複数のコンポーネントから利用でき、今後の機能追加や拡張が容易です。

デメリット

  • 設計の複雑さ
    非同期処理をRedux Toolkitでラップするためのコードが増え、初学者には理解が難しくなる場合があります。

  • 学習コスト
    Redux Toolkit自体は従来のReduxよりもシンプルですが、Supabaseの非同期処理との組み合わせとなると、実装パターンを理解するまでに時間がかかる可能性があります。

  • オーバーヘッド
    小規模なプロジェクトの場合、Reduxによるグローバル状態管理はオーバーヘッドとなる場合があります。プロジェクトの規模に応じて、Context APIやReact Queryなど他の選択肢も検討すべきです。

まとめ

ExpoプロジェクトでSupabaseを利用する場合、Supabaseの公式クライアントを直接使ってCRUD操作を行うことも可能ですが、アプリ全体の状態管理が複雑になってくると、Redux Toolkitによるグローバル管理が有効です。今回の実装例では、以下の流れを示しました。

  1. Supabaseクライアントの初期化

    • 詳細は公式のDOCSを参照してください。
  2. 非同期アクションの定義

    • createAsyncThunkを使ってSupabaseからのデータ取得・更新をラップする。
  3. Redux Sliceの作成

    • extraReducersで各状態(loading, succeeded, failed)を管理する。
  4. コンポーネントでの利用

    • useDispatchuseSelectorで非同期アクションを呼び出し、グローバルな状態を利用する。

このアプローチにより、Supabase側のデータ操作結果をグローバルに管理し、複数のコンポーネントで一貫した状態を扱うことができます。
用途やプロジェクト規模に合わせ、必要なツールを選択して実装していくと良いでしょう。

参考文献

Discussion