状態管理とデータ
React の常識(useState / useContext)はそのまま使える。
加えて RN 固有の事情として、サーバーデータ、永続化、オフライン対応を考える必要がある。
状態の3層
- UI ローカル状態 — そのコンポーネントだけ。
useState/useReducer。 - クライアント全体の共有状態 — テーマ、認証、設定など。Zustand / Jotai / Context。
- サーバー状態 — API から取ってくるデータ。React Query / SWR で管理。
「なんでも Redux」は時代遅れ。3 層に分けて、層ごとに適した道具を選ぶのが現代流。
1. UI ローカル: useState / useReducer
Web の React と同じ。説明省略。
2. クライアント共有: Zustand
2026 現在、Web/Native ともに人気が高い軽量ステートライブラリ。 Provider 不要、TS 親和性が高い、コードが短い。
npm install zustand
import { create } from "zustand"
interface AuthState {
user: User | null
setUser: (u: User | null) => void
signOut: () => void
}
export const useAuth = create<AuthState>((set) => ({
user: null,
setUser: (user) => set({ user }),
signOut: () => set({ user: null }),
}))
// 使う
function Profile() {
const user = useAuth((s) => s.user) // セレクタで購読
const signOut = useAuth((s) => s.signOut)
return user
? <Pressable onPress={signOut}><Text>サインアウト ({user.name})</Text></Pressable>
: null
}
Zustand を永続化
persist ミドルウェアで AsyncStorage / MMKV に自動保存。
import { create } from "zustand"
import { persist, createJSONStorage } from "zustand/middleware"
import AsyncStorage from "@react-native-async-storage/async-storage"
export const useSettings = create(
persist(
(set) => ({
theme: "dark",
setTheme: (theme: string) => set({ theme }),
}),
{
name: "settings",
storage: createJSONStorage(() => AsyncStorage),
},
),
)
3. サーバー状態: React Query (TanStack Query)
API データ管理のデファクトスタンダード。キャッシュ・再取得・エラーハンドリング・楽観的更新を一通り。
npm install @tanstack/react-query
セットアップ
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import { Slot } from "expo-router"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 30_000, // 30秒
retry: 1,
},
},
})
export default function RootLayout() {
return (
<QueryClientProvider client={queryClient}>
<Slot />
</QueryClientProvider>
)
}
fetch する
import { useQuery } from "@tanstack/react-query"
function PostList() {
const { data, isLoading, error, refetch, isRefetching } = useQuery({
queryKey: ["posts"],
queryFn: () => fetch("https://api.example.com/posts").then(r => r.json()),
})
if (isLoading) return <ActivityIndicator />
if (error) return <Text>エラー</Text>
return (
<FlatList
data={data}
keyExtractor={(p) => p.id}
renderItem={({ item }) => <PostRow post={item} />}
refreshing={isRefetching}
onRefresh={refetch}
/>
)
}
更新する(mutation)
import { useMutation, useQueryClient } from "@tanstack/react-query"
function NewPost() {
const qc = useQueryClient()
const mutation = useMutation({
mutationFn: (post: NewPost) =>
fetch("/api/posts", { method: "POST", body: JSON.stringify(post) }),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ["posts"] }) // 一覧を再取得
},
})
return <Button onPress={() => mutation.mutate({ title: "..." })} />
}
無限スクロール
import { useInfiniteQuery } from "@tanstack/react-query"
const { data, fetchNextPage, hasNextPage } = useInfiniteQuery({
queryKey: ["feed"],
queryFn: ({ pageParam }) => fetchPage(pageParam),
initialPageParam: 0,
getNextPageParam: (last) => last.nextCursor,
})
<FlatList
data={data?.pages.flatMap(p => p.items) ?? []}
onEndReached={() => hasNextPage && fetchNextPage()}
/>
永続化(Storage)
AsyncStorage(標準)
小さなキー/バリュー保存。非同期なので使う側はちょっと面倒。
npx expo install @react-native-async-storage/async-storage
import AsyncStorage from "@react-native-async-storage/async-storage"
await AsyncStorage.setItem("key", "value")
const v = await AsyncStorage.getItem("key")
await AsyncStorage.removeItem("key")
MMKV(推奨)
Facebook 製の同期で爆速な KV ストア。AsyncStorage の事実上の上位互換。 Dev Build が必要(ネイティブモジュールのため Expo Go では動かない)。
npx expo install react-native-mmkv
import { MMKV } from "react-native-mmkv"
const storage = new MMKV()
storage.set("user.id", 1) // 同期!
storage.getNumber("user.id")
storage.set("user.name", "alice")
storage.getString("user.name")
storage.delete("user.id")
expo-secure-store
機密情報(トークン、パスワード)を OS のキーチェーンに保存。サイズ制限あり(iOS 2KB 程度)。
import * as SecureStore from "expo-secure-store"
await SecureStore.setItemAsync("token", "abc123")
const token = await SecureStore.getItemAsync("token")
SQLite(expo-sqlite)
ローカル DB。複雑なデータや関係を扱う時。2026 現在は Drizzle ORM との組み合わせが人気。
import * as SQLite from "expo-sqlite"
const db = await SQLite.openDatabaseAsync("app.db")
await db.execAsync(`CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT)`)
await db.runAsync("INSERT INTO users (name) VALUES (?)", ["alice"])
const rows = await db.getAllAsync<{ id: number, name: string }>("SELECT * FROM users")
オフライン対応
モバイルは常にオフラインを考慮する。基本パターン:
- サーバー読み込み → React Query が自動でキャッシュ。
networkMode: "always"で離線時もキャッシュ表示。 - 書き込み → 楽観的更新 + 失敗時に再送キュー。
- ローカルファースト(PowerSync / WatermelonDB / RxDB)→ 本格的にやるなら専用ライブラリ。
オンライン状態
import NetInfo from "@react-native-community/netinfo"
const state = await NetInfo.fetch()
console.log(state.isConnected, state.type)
NetInfo.addEventListener((s) => {
// ネットワーク状態が変わった
})
認証
SecureStore + Zustand + React Query の組み合わせが定石:
- ログイン後、トークンを
SecureStoreに保存 - Zustand に
{ token, user }を持つ - API 呼び出しの fetch ラッパでトークンを Authorization に付ける
- 401 が返ったら自動でリフレッシュ or サインアウト
OAuth / Apple Sign-In / Google Sign-In
expo-auth-session— 汎用的な OAuth フローexpo-apple-authentication— Sign in with Apple@react-native-google-signin/google-signin— Google ログイン- Clerk / Auth0 / Firebase Auth / Supabase Auth など SaaS の SDK もある
Context + Reducer の使い所
Zustand を入れたくない小規模アプリや、screen 単位で完結する状態には Context + Reducer で十分。
Jotai / Recoil(参考)
atom 単位でリアクティブに状態を管理するライブラリ。UI コンポーネント単位の状態に向く。 Zustand と比べると好みが分かれる。
状態を永続化したい / 復元したい時の判断
| 用途 | 使うもの |
|---|---|
| テーマ / 言語 / 設定 | MMKV or AsyncStorage |
| 認証トークン | SecureStore |
| サーバーから取れる/再取得できるデータ | React Query のキャッシュで十分 |
| ローカルだけのリストやノート | SQLite |
| 大きなファイル(画像・動画) | FileSystem(Expo SDK) |
| 状態のスナップショットを毎回 | Zustand persist |
Expo Router は将来的に React Server Components の対応も進める予定(Universal RSC)。
tRPC を組み合わせて型安全な API クライアントを作るパターンも増えている。
普通の JSON API + React Query が依然として実用上のメインラインだが、定期的に動向はチェック。