React Nativeは、JavaScript(またはTypeScript)でiOS・Androidアプリを同時に作れる仕組みです。
Expoはその環境構築を自動化してくれるツールで、コマンド一発でアプリを実機で動かせるのが最大の特徴です。
この記事では、Windows(WSL)上に、Expoプロジェクトを構築してToDoアプリを作りながら、React Nativeの仕組みと基礎的なコードの書き方を学びます。
ToDoアプリを作りながら学ぶReact Nativeの基礎

Expoプロジェクトの作成
最初に、Expo DocumentのGet started->Create a projectに従って、Expoプロジェクトを作成します。
ドキュメントに従って、npx create-expo-app@latestを入力します。
What is your app named? …を表示して、プロジェクト名入力待ちになるので、今回は、 my-todo-app(任意のプロジェクト名)を入力します。
プロジェクト名を入力すると、Expoプロジェクトのインストールが始まり、この画面の最後まで進みます。
$ npx create-expo-app@latest
Creating an Expo project using the default template.
To choose from all available templates (https://github.com/expo/expo/tree/main/templates) pass in the --template arg:
$ npx create-expo-app --template
To choose from all available examples (https://github.com/expo/examples) pass in the --example arg:
$ npx create-expo-app --example
✔ What is your app named? … my-todo-app
✔ Downloaded and extracted project files.
> npm install
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated rimraf@3.0.2: Rimraf versions prior to v4 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
added 988 packages, and audited 989 packages in 38s
183 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
✅ Your project is ready!
To run your project, navigate to the directory and run one of the following npm commands.
- cd my-todo-app
- npm run android
- npm run ios # you need to use macOS to build the iOS project - use the Expo app if you need to do iOS development without a Mac
- npm run webExpoプロジェクトのインストールが完了すると、以下のフォルダーやファイルが作成されます。

スマホ(Iphone)にExpo Goアプリをインストールする
App Storeを開き、検索バーに「Expo Go」と入力してて、検索結果からExpo Goアプリ(開発元はExponent, Inc.)を見つけます。
アプリ名の横にある「入手」ボタンをタップして、インストールを行います。
この後、React Nativeの開発サーバーを起動すると、実機にインストールしたExpo Goアプリで、アプリの動作確認ができます。
Anroid Studioのスマホエミュレーターをインストールすると、スマホエミュレーターでReact Nativeで作成されたアプリの動作確認ができます。
Expo開発サーバーを起動する
開発サーバーを起動するには、ExpoドキュメントのStart a development serverに従って、次のコマンドを実行します。
npx expo startエラーが生じて、起動できない場合は、ExpoドキュメントのHaving problems?に従って、以下のコマンドを実行します。
npx expo start --tunnel開発サーバーが正常に起動すると以下の様なQRコードが表示されます。

(図は、セキュリティーの関係で赤色のバーで隠してあります。)
スマホ(Iphone)のカメラで読み取り、黄色のExpoGoで開くをタップします。

Welcome!👋画面の表示
React Nativeのプログラムが正常に読み込まれると、下記のWelcome!画面が表示されます。

Welcome!画面は、(tabs)フォルダーの中のindex.tsxが表示されています。
(Expo Goは、appフォルダの中にindex.tsxがない場合は、(tabs)フォルダーの中のindex.tsxを起動します。)

(tabs)フォルダーの中には、index.tsxとexplore.tsxが存在し、この2つのファイルを画面下段のタブで切り替えて表示する作りとなっています。
プロジェクトをリセットする
Expoドキュメントに従い次のステップへ進みます。
次のステップでは、この定型コードを削除して、新しいプロジェクトを最初から始めるために、次のコマンドを入力します。
npm run reset-projectこのコマンドでは、app内の現在のファイルが、app-exampleフォルダに移され、新しいindex.tsxファイルを含む新しいappフォルダが作成されます。
# コマンド入力後に、削除するファイルを/app-exampleに移動するか聞いてくるので、移動する場合はYと応答する。
# 使用する予定がない場合は、nで応答する。
$ npm run reset-project
> my-todo-app@1.0.0 reset-project
> node ./scripts/reset-project.js
Do you want to move existing files to /app-example instead of deleting them? (Y/n): Y
📁 /app-example directory created.
➡️ /app moved to /app-example/app.
➡️ /components moved to /app-example/components.
➡️ /hooks moved to /app-example/hooks.
➡️ /constants moved to /app-example/constants.
➡️ /scripts moved to /app-example/scripts.
📁 New /app directory created.
📄 app/index.tsx created.
📄 app/_layout.tsx created.
✅ Project reset complete. Next steps:
1. Run `npx expo start` to start a development server.
2. Edit app/index.tsx to edit the main screen.
3. Delete the /app-example directory when you're done referencing it.プロジェクトをリセット後のフォルダ構成は、下記の様になります。
(app内の現在のファイルを、app-exampleに移動した場合。)

また、下記の様にWelcome!画面は、Edit app/index.tsx to edit this screen.メッセージを表示する新しいindex.tsxファイルの内容です。

ToDoブアプリの作成
AsyncStorage機能を使用して、追加・削除・アーカイブ機能を持つ基本的なToDoアプリを作成します。
ディレクトリとファイル構成
# appディレクトリ配下に以下のファイルとフォルダを作成します。
app/
├─ _layout.tsx
└─ (tabs)/
├─ _layout.tsx
├─ index.tsx
├─ todo.tsx
└─ archive.tsx# ファイル構造の解説
app/_layout.tsx:
アプリ全体の共通レイアウト(ルートレイアウト)で、アプリの一番外側の枠組みを定義しています。
app/(tabs)/ (フォルダ)
(tabs)フォルダは、URLのパス(階層)には影響しません。
単にファイルを整理・グループ化するためのフォルダです。
app/(tabs)/_layout.tsx
このフォルダ内にある画面(index, todo, archive)を「タブ切り替え」で表示するための設定ファイルです。
app/(tabs)/index.tsx
タブのデフォルト画面(ホーム画面)で、ユーザーがアプリを開いたときやタブバーの「index」タブをタップしたときに表示される画面です。
app/(tabs)/todo.tsx
タブバーの「todo」タブをタップしたときに表示される画面です。
app/(tabs)/archive.tsx
タブバーの「archive」タブをタップしたときに表示される画面です。
各ファイルの内容
各ファイルの内容を以下の内容に置き換えます。
app/_layout.tsx
アプリ全体のレイアウト設定するために、app/_layout.tsx を以下の内容で置き換えます。
この設定により、「下タブ付きアプリ+モーダル画面」の構成ができます。headerShown: false にしているので、上部タイトルバーを自分で作れる自由設計です。
// app/_layout.tsx
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useColorScheme } from 'react-native';
import 'react-native-reanimated';
export default function RootLayout() {
const colorScheme = useColorScheme();
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack initialRouteName="(tabs)">
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: "modal", title: "Modal" }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
}
app/(tabs)/_layout.tsx
タブレイアウトの設定のために、app/(tabs)/_layout.tsx を作成します。
この設定により、画面下部のタブにindex、todo、archiveのタブが表示されます。
// app/(tabs)/_layout.tsx
import { Ionicons } from "@expo/vector-icons";
import { Tabs } from "expo-router";
import React from "react";
export default function TabLayout() {
return (
<Tabs
initialRouteName="index"
screenOptions={{
headerShown: false,
tabBarActiveTintColor: "#007aff",
}}
>
<Tabs.Screen
name="index"
options={{
title: "Home",
tabBarIcon: ({ color, size }) => (
<Ionicons name="home-outline" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="todo"
options={{
title: "ToDo",
tabBarIcon: ({ color, size }) => (
<Ionicons name="checkmark-done-outline" color={color} size={size} />
),
}}
/>
<Tabs.Screen
name="archive"
options={{
title: "Archive",
tabBarIcon: ({ color, size }) => (
<Ionicons name="archive-outline" color={color} size={size} />
),
}}
/>
</Tabs>
);
}
app/(tabs)/index.tsx
🏠アプリ立ち上げ時に表示されるデフォルト画面(ホーム画面)です。
// app/(tabs)/index.tsx
import { Ionicons } from "@expo/vector-icons";
import { useTheme } from "@react-navigation/native";
import { StyleSheet, Text, View } from "react-native";
export default function HomeScreen() {
const { colors, dark } = useTheme();
const palette = {
bg: colors.background,
text: colors.text,
sub: dark ? "#A1A1AA" : "#6B7280",
card: dark ? "#111827" : "#FFFFFF",
border: dark ? "#1F2937" : "#E5E7EB",
accent: "#007AFF",
};
return (
<View style={[styles.container, { backgroundColor: palette.bg }]}>
{/* ヒーロー(上の大きな案内) */}
<View style={[styles.hero, { backgroundColor: palette.card, borderColor: palette.border }]}>
<View style={styles.heroIconWrap}>
<Ionicons name="rocket-outline" size={28} color={palette.accent} />
</View>
<Text style={[styles.heroTitle, { color: palette.text }]}>
React Native × Expo Go 入門
</Text>
<Text style={[styles.heroSub, { color: palette.sub }]}>
スマホ実機で動かしながら学べる ToDo アプリを作ります
</Text>
</View>
{/* ガイドカード */}
<View style={[styles.card, { backgroundColor: palette.card, borderColor: palette.border }]}>
<View style={styles.cardRow}>
<Ionicons name="checkmark-circle-outline" size={20} color={palette.accent} />
<Text style={[styles.cardText, { color: palette.text }]}>
下の「ToDo」タブでタスクを追加
</Text>
</View>
<View style={styles.cardRow}>
<Ionicons name="archive-outline" size={20} color={palette.accent} />
<Text style={[styles.cardText, { color: palette.text }]}>
完了したらアーカイブへ移動
</Text>
</View>
<View style={styles.cardRow}>
<Ionicons name="arrow-undo-outline" size={20} color={palette.accent} />
<Text style={[styles.cardText, { color: palette.text }]}>
Archive で復元して戻せます
</Text>
</View>
</View>
{/* フッター */}
<Text style={[styles.footer, { color: palette.sub }]}>
まずは ToDo タブへ進んでみましょう👇
</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
gap: 14,
justifyContent: "center",
},
hero: {
borderWidth: 1,
borderRadius: 18,
padding: 18,
alignItems: "center",
gap: 8,
// iOS shadow
shadowOpacity: 0.08,
shadowRadius: 12,
shadowOffset: { width: 0, height: 4 },
// Android shadow
elevation: 2,
},
heroIconWrap: {
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: "#E6F0FF",
alignItems: "center",
justifyContent: "center",
marginBottom: 2,
},
heroTitle: {
fontSize: 22,
fontWeight: "800",
letterSpacing: 0.2,
},
heroSub: {
fontSize: 14,
textAlign: "center",
lineHeight: 20,
},
card: {
borderWidth: 1,
borderRadius: 16,
padding: 14,
gap: 10,
// iOS shadow
shadowOpacity: 0.06,
shadowRadius: 10,
shadowOffset: { width: 0, height: 3 },
// Android shadow
elevation: 1,
},
cardRow: {
flexDirection: "row",
alignItems: "center",
gap: 10,
},
cardText: {
fontSize: 16,
lineHeight: 22,
fontWeight: "600",
flex: 1,
},
footer: {
textAlign: "center",
fontSize: 13,
marginTop: 6,
},
});
app/(tabs)/todo.tsx
📝タスクを追加/完了/削除する機能を持つToDo画面です。
// app/(tabs)/todo.tsx
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useEffect, useMemo, useState, useCallback } from "react";
import {
Alert,
FlatList,
KeyboardAvoidingView,
Platform,
StyleSheet,
Text,
TextInput,
View,
Pressable,
SafeAreaView,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { useTheme, useFocusEffect } from "@react-navigation/native";
type Task = { id: string; title: string; done: boolean };
export default function TodoTab() {
const { colors, dark } = useTheme();
const [tasks, setTasks] = useState<Task[]>([]);
const [input, setInput] = useState("");
const [filter, setFilter] = useState<"all" | "undone" | "done">("all");
// ───────────────── 追加:タスク読み込み処理を関数化 ─────────────────
const loadTasks = useCallback(async () => {
const raw = await AsyncStorage.getItem("tasks");
setTasks(raw ? JSON.parse(raw) : []);
}, []);
// 初回マウント時に1回読み込む
useEffect(() => {
loadTasks();
}, [loadTasks]);
// タブに戻ってきたときにも毎回読み込む(復元を反映させる)
useFocusEffect(
useCallback(() => {
loadTasks();
}, [loadTasks])
);
// ───────────────── ここまで共通読み込み処理 ─────────────────
const save = async (next: Task[]) => {
setTasks(next);
await AsyncStorage.setItem("tasks", JSON.stringify(next));
};
const addTask = async () => {
const title = input.trim();
if (!title) return;
const next = [{ id: String(Date.now()), title, done: false }, ...tasks];
await save(next);
setInput("");
};
const toggleTask = async (id: string) => {
const next = tasks.map((t) => (t.id === id ? { ...t, done: !t.done } : t));
await save(next);
};
const removeTask = (id: string) => {
Alert.alert("削除しますか?", "このタスクを削除します", [
{ text: "キャンセル", style: "cancel" },
{
text: "削除",
style: "destructive",
onPress: async () => {
const next = tasks.filter((t) => t.id !== id);
await save(next);
},
},
]);
};
const archiveDone = async () => {
const doneTasks = tasks.filter((t) => t.done);
if (!doneTasks.length) return;
const raw = await AsyncStorage.getItem("archives");
const archives: Task[] = raw ? JSON.parse(raw) : [];
await AsyncStorage.setItem("archives", JSON.stringify([...doneTasks, ...archives]));
const remaining = tasks.filter((t) => !t.done);
await save(remaining);
};
const clearDone = () => {
if (!tasks.some((t) => t.done)) return;
Alert.alert("完了済みを削除しますか?", "未完は残ります。", [
{ text: "キャンセル", style: "cancel" },
{
text: "削除する",
style: "destructive",
onPress: async () => {
const next = tasks.filter((t) => !t.done);
await save(next);
},
},
]);
};
const clearAll = () => {
Alert.alert("すべて削除しますか?", "この操作は取り消せません。", [
{ text: "キャンセル", style: "cancel" },
{ text: "削除する", style: "destructive", onPress: async () => await save([]) },
]);
};
const sortedTasks = useMemo(() => {
let list = [...tasks];
if (filter === "undone") list = list.filter((t) => !t.done);
if (filter === "done") list = list.filter((t) => t.done);
const undone = list.filter((t) => !t.done).sort((a, b) => Number(b.id) - Number(a.id));
const done = list.filter((t) => t.done).sort((a, b) => Number(a.id) - Number(b.id));
return [...undone, ...done];
}, [tasks, filter]);
const palette = {
bg: colors.background,
text: colors.text,
sub: dark ? "#9AA0A6" : "#6b7280",
card: dark ? "#1f2937" : "#ffffff",
border: dark ? "#374151" : "#ececec",
accent: "#007aff",
danger: "#ef4444",
chipBg: dark ? "#111827" : "#f3f4f6",
inputBg: dark ? "#111827" : "#f9fafb",
shadow: "#00000040",
};
return (
<KeyboardAvoidingView
style={{ flex: 1, backgroundColor: palette.bg }}
behavior={Platform.OS === "ios" ? "padding" : undefined}
>
<SafeAreaView style={{ flex: 1 }}>
{/* ヘッダー */}
<View style={styles.header}>
<View>
<Text style={[styles.title, { color: palette.text }]}>ToDo</Text>
<Text style={{ color: palette.sub }}>
未完 {tasks.filter((t) => !t.done).length} ・ すべて {tasks.length}
</Text>
</View>
<View style={styles.headerActions}>
<Pill onPress={archiveDone} icon="archive-outline" label="アーカイブ" />
<View style={{ width: 8 }} />
<MenuIcon icon="trash-outline" onPress={clearDone} color={palette.sub} />
<View style={{ width: 4 }} />
<MenuIcon icon="trash" onPress={clearAll} color={palette.danger} />
</View>
</View>
{/* チップフィルタ */}
<View style={styles.chips}>
<Chip active={filter === "all"} onPress={() => setFilter("all")} label="すべて" />
<Chip
active={filter === "undone"}
onPress={() => setFilter("undone")}
label="未完"
/>
<Chip active={filter === "done"} onPress={() => setFilter("done")} label="完了" />
</View>
{/* リスト */}
<FlatList
data={sortedTasks}
keyExtractor={(item) => item.id}
contentContainerStyle={{ paddingHorizontal: 16, paddingBottom: 120 }}
ListEmptyComponent={
<View style={styles.empty}>
<Ionicons name="checkmark-circle-outline" size={48} color={palette.sub} />
<Text style={{ color: palette.sub, marginTop: 8 }}>
まだタスクがありません
</Text>
</View>
}
renderItem={({ item }) => (
<View
style={[
styles.card,
{
backgroundColor: palette.card,
borderColor: palette.border,
shadowColor: palette.shadow,
},
]}
>
<Pressable onPress={() => toggleTask(item.id)} style={styles.rowLeft}>
<Ionicons
name={item.done ? "checkbox" : "square-outline"}
size={22}
color={item.done ? palette.accent : palette.sub}
/>
<Text
style={[
styles.cardText,
{
color: item.done ? palette.sub : palette.text,
textDecorationLine: item.done ? "line-through" : "none",
},
]}
numberOfLines={2}
>
{item.title}
</Text>
</Pressable>
<Pressable
onPress={() => removeTask(item.id)}
hitSlop={10}
style={({ pressed }) => [{ opacity: pressed ? 0.6 : 1 }]}
>
<Ionicons name="trash-outline" size={20} color={palette.sub} />
</Pressable>
</View>
)}
/>
{/* 入力ボックス(固定) */}
<View
style={[
styles.inputBar,
{
backgroundColor: palette.card,
borderTopColor: palette.border,
shadowColor: palette.shadow,
},
]}
>
<View
style={[
styles.inputWrap,
{ backgroundColor: palette.inputBg, borderColor: palette.border },
]}
>
<Ionicons name="add-outline" size={20} color={palette.sub} />
<TextInput
value={input}
onChangeText={setInput}
placeholder="タスクを入力"
placeholderTextColor={palette.sub}
style={[styles.input, { color: palette.text }]}
onSubmitEditing={addTask}
returnKeyType="done"
/>
</View>
<Pressable
onPress={addTask}
style={({ pressed }) => [
styles.fab,
{ backgroundColor: palette.accent, opacity: pressed ? 0.8 : 1 },
]}
>
<Ionicons name="arrow-up" size={20} color="#fff" />
</Pressable>
</View>
</SafeAreaView>
</KeyboardAvoidingView>
);
}
/* ───────────────── UI小物 ───────────────── */
function Pill({
icon,
label,
onPress,
}: {
icon: any;
label: string;
onPress: () => void;
}) {
return (
<Pressable onPress={onPress} style={({ pressed }) => [pillStyles.base, pressed && { opacity: 0.8 }]}>
<Ionicons name={icon} size={16} color="#0a84ff" />
<Text style={pillStyles.text}>{label}</Text>
</Pressable>
);
}
function MenuIcon({
icon,
onPress,
color = "#6b7280",
}: {
icon: any;
onPress: () => void;
color?: string;
}) {
return (
<Pressable onPress={onPress} hitSlop={10} style={({ pressed }) => [{ opacity: pressed ? 0.6 : 1 }]}>
<Ionicons name={icon} size={20} color={color} />
</Pressable>
);
}
function Chip({
label,
active,
onPress,
}: {
label: string;
active?: boolean;
onPress: () => void;
}) {
return (
<Pressable
onPress={onPress}
style={({ pressed }) => [
chipStyles.base,
active ? chipStyles.active : chipStyles.inactive,
pressed && { opacity: 0.85 },
]}
>
<Text style={[chipStyles.text, active && chipStyles.textActive]}>{label}</Text>
</Pressable>
);
}
/* ───────────────── styles ───────────────── */
const styles = StyleSheet.create({
header: {
paddingHorizontal: 16,
paddingTop: 8,
paddingBottom: 12,
flexDirection: "row",
alignItems: "flex-end",
justifyContent: "space-between",
},
title: { fontSize: 28, fontWeight: "800" },
headerActions: { flexDirection: "row", alignItems: "center" },
chips: { flexDirection: "row", gap: 8, paddingHorizontal: 16, paddingBottom: 8 },
card: {
borderWidth: 1,
borderRadius: 16,
padding: 14,
marginBottom: 10,
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
shadowOpacity: 0.08,
shadowRadius: 12,
shadowOffset: { width: 0, height: 4 },
elevation: 2,
},
rowLeft: { flexDirection: "row", alignItems: "center", gap: 10, flex: 1 },
cardText: { fontSize: 16, lineHeight: 22, flex: 1 },
empty: { alignItems: "center", marginTop: 48 },
inputBar: {
position: "absolute",
left: 0,
right: 0,
bottom: 0,
borderTopWidth: StyleSheet.hairlineWidth,
padding: 12,
flexDirection: "row",
alignItems: "center",
gap: 10,
shadowOpacity: 0.06,
shadowRadius: 10,
shadowOffset: { width: 0, height: -2 },
elevation: 8,
},
inputWrap: {
flex: 1,
flexDirection: "row",
alignItems: "center",
gap: 8,
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 12,
height: 44,
},
input: { flex: 1, fontSize: 16 },
fab: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: "center",
justifyContent: "center",
},
});
const pillStyles = StyleSheet.create({
base: {
flexDirection: "row",
alignItems: "center",
gap: 6,
paddingHorizontal: 12,
height: 32,
backgroundColor: "#e6f0ff",
borderRadius: 999,
},
text: { color: "#0a84ff", fontWeight: "700" },
});
const chipStyles = StyleSheet.create({
base: {
paddingHorizontal: 12,
height: 30,
borderRadius: 999,
alignItems: "center",
justifyContent: "center",
borderWidth: 1,
},
active: { backgroundColor: "#111827", borderColor: "#111827" },
inactive: { backgroundColor: "#f3f4f6", borderColor: "#e5e7eb" },
text: { color: "#374151", fontWeight: "600", fontSize: 13 },
textActive: { color: "#fff" },
});
app/(tabs)/archive.tsx
📂完了済みをアーカイブするアーカイブ画面です。
// app/(tabs)/archive.tsx
import { useEffect, useState, useCallback } from "react";
import {
View, Text, StyleSheet, TouchableOpacity, Alert, FlatList, Button,
} from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";
import { useFocusEffect } from "@react-navigation/native";
type Task = { id: string; title: string; done: boolean };
export default function ArchiveTab() {
const [archives, setArchives] = useState<Task[]>([]);
const [refreshing, setRefreshing] = useState(false);
const load = async () => {
setRefreshing(true);
const raw = await AsyncStorage.getItem("archives");
setArchives(raw ? JSON.parse(raw) : []);
setRefreshing(false);
};
useFocusEffect(useCallback(() => { load(); }, []));
const restore = async (item: Task) => {
const rawTasks = await AsyncStorage.getItem("tasks");
const tasks = rawTasks ? JSON.parse(rawTasks) : [];
const nextArchives = archives.filter(t => t.id !== item.id);
await AsyncStorage.setItem("archives", JSON.stringify(nextArchives));
const restored = { ...item, done: false };
await AsyncStorage.setItem("tasks", JSON.stringify([restored, ...tasks]));
Alert.alert("復元しました");
};
const remove = (id: string) => {
Alert.alert("削除しますか?", "", [
{ text: "キャンセル", style: "cancel" },
{
text: "削除", style: "destructive",
onPress: async () => {
const next = archives.filter(t => t.id !== id);
await AsyncStorage.setItem("archives", JSON.stringify(next));
setArchives(next);
},
},
]);
};
return (
<View style={styles.container}>
<Text style={styles.title}>アーカイブ</Text>
<FlatList
data={archives.sort((a, b) => Number(b.id) - Number(a.id))}
keyExtractor={(item) => item.id}
onRefresh={load}
refreshing={refreshing}
renderItem={({ item }) => (
<View style={styles.item}>
<Text style={styles.itemText}>🗂 {item.title}</Text>
<View style={styles.actions}>
<TouchableOpacity onPress={() => restore(item)}>
<Text style={styles.link}>復元</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => remove(item.id)}>
<Text style={[styles.link, { color: "crimson" }]}>削除</Text>
</TouchableOpacity>
</View>
</View>
)}
ListEmptyComponent={
<Text style={{ textAlign: "center", color: "#888", marginTop: 24 }}>
アーカイブは空です
</Text>
}
/>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 16, backgroundColor: "#fff" },
title: { fontSize: 22, fontWeight: "bold", marginBottom: 12 },
item: {
flexDirection: "row",
alignItems: "center",
borderWidth: 1,
borderColor: "#eee",
borderRadius: 8,
padding: 10,
marginBottom: 8,
},
itemText: { fontSize: 16, flex: 1 },
actions: { flexDirection: "row" },
link: { marginLeft: 12, color: "#007aff", fontWeight: "bold" },
});
AsyncStorageのインストール
AsyncStorage とは “ローカルストレージ操作用の API で、React Native アプリにおける“永続的なローカルデータ保存”の標準的な手段 です。
AsyncStorageの機能
スマホ本体のストレージに「キーと値」でデータを保存する
- 永続保存(アプリを閉じても残る)
- 保存できるのは文字列(JSON を入れる場合は stringify)
- 小規模データ向け(ToDo リスト、設定値など)
React Native(Expo)では AsyncStorage は標準では含まれていないため、以下のコマンドでインストールします。
// AsyncStorageのインストール
npm install @react-native-async-storage/async-storage動作確認
React Nativeの開発サーバーを起動すると同様の手順で、ToDoアプリを起動します。
ToDoアプリが正常に起動したら、実機(iphone)で、以下の操作を行いToDoアプリの動作確認を行います。
- カメラを起動し、QRコードを読み取る
- 下のタブ「ToDo」を開く
- タスクを追加/完了/削除してみる
- 完了済みをアーカイブして「Archive」タブで確認する
- アーカイブからの復元操作を確認する



動作確認が終わったら
今回作成したExpoプロジェクトは、Gitリポジトリとなっており、インストール完了時点で、以下のコミットメッセージを付けてコミットされています。
Initial commitToDoアプリの動作確認が終わったら、機能追加や改造をする前に、現状をコミットしておきましょう。
動作確認が取れた状態をコミットしておくと、機能追加や改造でエラーが発生した場合でもいつでも、戻ってやり直すことが出来ます。
Gitに関しては、以下を参考にしてください。
Android エミュレーターの利用
Android エミュレーターで、動作確認を行いたい場合は、以下のページを参考にしてAndroid エミュレーターをインストールして確認してください。
参考:wslインストール
WSL(Windows Subsystem for Linux)とは、
Windows の中で Linux を“直接ネイティブ実行できる”仕組みのことです。

WSLは、現在のWindows(Windows 10 バージョン 2004以上、またはWindows 11)では、コマンド1つでに簡単にインストールできます。
インストール手順
手順 ①:PowerShellを管理者権限で開く
- スタートボタンを右クリックします。
- 「ターミナル (管理者)」 または 「Windows PowerShell (管理者)」 を選択します。
- 「このアプリがデバイスに変更を加えることを許可しますか?」と出たら「はい」を押します。
手順 ②:インストールコマンドを実行する
開いた画面(青または黒の画面)に、以下のコマンドを入力してEnterキーを押します。
wsl --installこのコマンドは以下の処理を自動で行います。
- WSLに必要なWindowsの機能の有効化
- Linuxカーネルのダウンロード
- WSL 2 を既定値に設定
- Ubuntu(標準のLinuxディストリビューション)のインストール
手順 ③:PCを再起動する
インストールが完了すると、画面に「再起動してください」という旨のメッセージが表示されます。
一度、Windowsを再起動します。
手順 ④:初期設定(ユーザー名とパスワード)
再起動後、自動的に黒い画面(Ubuntuのターミナル)が立ち上がります。
※立ち上がらない場合は、スタートメニューから「Ubuntu」を探してクリックしてください。
数分待つと、ユーザー作成を求められます。
- Enter new UNIX username:
- 好きなユーザー名を半角英数字で入力してEnter(例:
taro)。 - 注: Windowsのユーザー名と同じである必要はありません。
- 好きなユーザー名を半角英数字で入力してEnter(例:
- New password:
- パスワードを入力してEnter。
- Retype new password:
- もう一度パスワードを入力してEnter。



