【ハンズオン】React Native + Supabaseで作る!認証付きTodoアプリ開発入門
はじめに
React Nativeの学習を始めたいけれど、何から作れば良いか分からない...。そんなあなたのために、モダンなバックエンドサービス Supabase を利用した「認証付きTodoアプリ」の開発手順を、ゼロから丁寧に解説します。
この記事を終える頃には、あなたは認証機能とデータベースを連携させた、本格的なクロスプラットフォームアプリの作り方をマスターしているでしょう。
この記事で作るもの
- 認証機能: メールアドレスとパスワードでのサインアップ&ログイン
- Todo管理: ログインユーザーに紐づいたTodoの登録・一覧表示・更新・削除 (CRUD)
- リアルタイム更新: データベースの変更がアプリに即座に反映される機能
ステップ0: 開発の前に(前提知識)
-
JavaScript (ES6+):
async/await
を含め、基本的な文法を理解していること。 -
Reactの基礎:
useState
,useEffect
などの基本的なHooksを理解していること。
ステップ1: 開発環境の準備
まず、Expoを使ったReact Nativeの開発環境を整えます。
-
Node.jsのインストール: 公式サイトからLTS版をインストールします。
-
Expo CLIのインストール: ターミナルで
npm install -g expo-cli
を実行します。 -
プロジェクト作成: ターミナルで以下のコマンドを実行します。
expo init my-todo-app --template blank --npm cd my-todo-app
-
開発サーバーの起動:
npm start
を実行します。 -
実機確認: スマートフォンに Expo Go アプリをインストールし、ターミナルに表示されたQRコードを読み取ります。「Open up App.js...」と表示されれば成功です。
ステップ2: Supabaseのセットアップ
次に、アプリのバックエンドとなるSupabaseを準備します。
-
Supabase公式サイトでサインアップし、新しいプロジェクトを作成します。
-
テーブルの作成: プロジェクトページの「SQL Editor」に移動し、以下のクエリを実行して
todos
テーブルを作成します。-- todosテーブルの作成 create table todos ( id bigint generated by default as identity primary key, user_id uuid references auth.users not null, task text check (char_length(task) > 0), is_complete boolean default false, inserted_at timestamp with time zone default timezone('utc'::text, now()) not null ); -- RLS (Row Level Security) の有効化 alter table todos enable row level security; -- 自分のTodoのみを閲覧できるポリシー create policy "Individuals can view their own todos." on todos for select using (auth.uid() = user_id); -- 自分のTodoのみを追加できるポリシー create policy "Individuals can insert their own todos." on todos for insert with check (auth.uid() = user_id); -- 自分のTodoのみを更新できるポリシー create policy "Individuals can update their own todos." on todos for update using (auth.uid() = user_id); -- 自分のTodoのみを削除できるポリシー create policy "Individuals can delete their own todos." on todos for delete using (auth.uid() = user_id);
RLS (Row Level Security) は、「ユーザーは自分自身のデータしか操作できない」という重要なセキュリティルールです。
-
APIキーの取得: 「Project Settings」>「API」で、
Project URL
とanon
public
キーを控えておきます。
ステップ3: React NativeにSupabaseを導入
-
ライブラリのインストール: ターミナルで以下のコマンドを実行します。
npm install @supabase/supabase-js react-native-url-polyfill @react-native-async-storage/async-storage
-
@supabase/supabase-js
: Supabaseの公式クライアント -
react-native-url-polyfill
: Expo環境でSupabaseを動作させるために必要 -
@react-native-async-storage/async-storage
: ログインセッションを永続化するために必要
-
-
Supabaseクライアントの初期化: プロジェクトのルートに
lib/supabase.ts
ファイルを作成し、以下のように編集します。lib/supabase.tsimport 'react-native-url-polyfill/auto'; import AsyncStorage from '@react-native-async-storage/async-storage'; import { createClient } from '@supabase/supabase-js'; const supabaseUrl = 'YOUR_SUPABASE_URL'; // 先ほど控えたURL const supabaseAnonKey = 'YOUR_SUPABASE_ANON_KEY'; // 先ほど控えたanonキー export const supabase = createClient(supabaseUrl, supabaseAnonKey, { auth: { storage: AsyncStorage, autoRefreshToken: true, persistSession: true, detectSessionInUrl: false, }, });
ステップ4: 認証機能の実装
ログイン状態をアプリ全体で管理するために、Context APIを利用します。
-
AuthProvider
の作成:components/AuthProvider.tsx
を作成します。components/AuthProvider.tsximport React, { useState, useEffect, createContext, useContext, PropsWithChildren } from 'react'; import { Session } from '@supabase/supabase-js'; import { supabase } from '../lib/supabase'; type AuthContextType = { session: Session | null; }; const AuthContext = createContext<AuthContextType>({ session: null }); export const AuthProvider = ({ children }: PropsWithChildren) => { const [session, setSession] = useState<Session | null>(null); useEffect(() => { supabase.auth.getSession().then(({ data: { session } }) => { setSession(session); }); const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setSession(session); }); return () => subscription.unsubscribe(); }, []); return <AuthContext.Provider value={{ session }}>{children}</AuthContext.Provider>; }; export const useAuth = () => useContext(AuthContext);
-
認証画面の作成:
components/Auth.tsx
を作成します。components/Auth.tsximport React, { useState } from 'react'; import { Alert, StyleSheet, View, TextInput, Button } from 'react-native'; import { supabase } from '../lib/supabase'; export default function Auth() { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [loading, setLoading] = useState(false); async function signInWithEmail() { setLoading(true); const { error } = await supabase.auth.signInWithPassword({ email, password }); if (error) Alert.alert(error.message); setLoading(false); } async function signUpWithEmail() { setLoading(true); const { error } = await supabase.auth.signUp({ email, password }); if (error) Alert.alert(error.message); setLoading(false); } return ( <View style={styles.container}> <TextInput label="Email" onChangeText={setEmail} value={email} placeholder="email@address.com" autoCapitalize="none" /> <TextInput label="Password" onChangeText={setPassword} value={password} secureTextEntry={true} placeholder="Password" autoCapitalize="none" /> <Button title="Sign in" disabled={loading} onPress={signInWithEmail} /> <Button title="Sign up" disabled={loading} onPress={signUpWithEmail} /> </View> ); } const styles = StyleSheet.create({ /* ... */ });
-
App.tsx
の修正:App.tsx
を修正し、認証状態で表示を切り替えます。App.tsximport React from 'react'; import { AuthProvider, useAuth } from './components/AuthProvider'; import Auth from './components/Auth'; import TodoList from './components/TodoList'; // 次に作成します const AppContainer = () => { const { session } = useAuth(); return session && session.user ? <TodoList /> : <Auth />; }; export default function App() { return ( <AuthProvider> <AppContainer /> </AuthProvider> ); }
ステップ5: Todo機能の実装
components/TodoList.tsx
を作成し、TodoのCRUD処理を実装します。
import React, { useState, useEffect } from 'react';
import { View, TextInput, Button, FlatList, Text, TouchableOpacity, StyleSheet } from 'react-native';
import { supabase } from '../lib/supabase';
import { useAuth } from './AuthProvider';
type Todo = { id: number; task: string; is_complete: boolean; };
export default function TodoList() {
const { session } = useAuth();
const [todos, setTodos] = useState<Todo[]>([]);
const [newTask, setNewTask] = useState('');
useEffect(() => {
fetchTodos();
}, []);
const fetchTodos = async () => {
const { data, error } = await supabase.from('todos').select('*').order('id');
if (error) console.log('error', error);
else setTodos(data as Todo[]);
};
const addTodo = async () => {
if (!newTask.trim()) return;
const { data, error } = await supabase.from('todos').insert({ task: newTask, user_id: session!.user.id }).select();
if (error) console.log('error', error);
else setTodos([...todos, data[0]]);
setNewTask('');
};
const toggleComplete = async (id: number, is_complete: boolean) => {
const { error } = await supabase.from('todos').update({ is_complete: !is_complete }).match({ id });
if (error) console.log('error', error);
else setTodos(todos.map(todo => todo.id === id ? { ...todo, is_complete: !is_complete } : todo));
};
const deleteTodo = async (id: number) => {
const { error } = await supabase.from('todos').delete().match({ id });
if (error) console.log('error', error);
else setTodos(todos.filter(todo => todo.id !== id));
};
return (
<View style={styles.container}>
<View style={styles.inputContainer}>
<TextInput onChangeText={setNewTask} value={newTask} placeholder="Add a new task" />
<Button title="Add" onPress={addTodo} />
</View>
<FlatList
data={todos}
keyExtractor={(item) => item.id.toString()}
renderItem={({ item }) => (
<View style={styles.todoItem}>
<TouchableOpacity onPress={() => toggleComplete(item.id, item.is_complete)}>
<Text style={item.is_complete ? styles.completed : {}}>{item.task}</Text>
</TouchableOpacity>
<Button title="Delete" onPress={() => deleteTodo(item.id)} color="red" />
</View>
)}
/>
<Button title="Sign Out" onPress={() => supabase.auth.signOut()} />
</View>
);
}
const styles = StyleSheet.create({ /* ... */ });
ステップ6: リアルタイム機能で自動更新
TodoList.tsx
の useEffect
にSupabaseのリアルタイム購読機能を追加します。
// ... useEffectの中身を修正
useEffect(() => {
fetchTodos();
const channel = supabase.channel('todos');
const subscription = channel
.on('postgres_changes', { event: '*', schema: 'public', table: 'todos' }, (payload) => {
console.log('Change received!', payload);
fetchTodos(); // 変更があったらリストを再取得
})
.subscribe();
return () => {
supabase.removeChannel(channel);
};
}, []);
// ...
これで、データベースに変更があると、アプリの表示が自動で更新されるようになりました!
ステップ7: ビルドと公開
開発したアプリは、Expoのクラウドサービス EAS (Expo Application Services) を使ってビルドできます。
npm install -g eas-cli
eas login
eas build:configure
eas build --platform all
これにより、App StoreやGoogle Playに提出するためのファイルが生成されます。
おわりに
このハンズオンでは、React NativeとSupabaseを使って、基本的ながらも本格的な機能を持つTodoアプリを作成しました。認証、データベース操作、リアルタイム更新といった、現代的なアプリに不可欠な要素を体験できたはずです。
ここからさらにUIをリッチにしたり、エラーハンドリングを丁寧にするなど、改善の余地はたくさんあります。ぜひこのアプリをベースに、あなただけのオリジナル機能を追加してみてください!
Discussion