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の公式クライアントライブラリを利用して初期化します。
詳しくは公式のサイトの手順に従ってください。
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コンポーネント内で、useDispatch
とuseSelector
を利用して、非同期アクションを発火し、状態を取得します。
// 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によるグローバル管理が有効です。今回の実装例では、以下の流れを示しました。
-
Supabaseクライアントの初期化
- 詳細は公式のDOCSを参照してください。
-
非同期アクションの定義
-
createAsyncThunk
を使ってSupabaseからのデータ取得・更新をラップする。
-
-
Redux Sliceの作成
- extraReducersで各状態(loading, succeeded, failed)を管理する。
-
コンポーネントでの利用
-
useDispatch
とuseSelector
で非同期アクションを呼び出し、グローバルな状態を利用する。
-
このアプローチにより、Supabase側のデータ操作結果をグローバルに管理し、複数のコンポーネントで一貫した状態を扱うことができます。
用途やプロジェクト規模に合わせ、必要なツールを選択して実装していくと良いでしょう。
Discussion