React Native×Expoに入門する
はじめに
どうも!最近、手を動かしながら学ぶことで理解が深まることを実感しているshigeです。
この記事では最近注目を集めているReact Nativeを学ぶために、ToDoアプリを作成する過程を解説します。
私と同じように、これからReact Nativeを始める方にとって、少しでも役に立つ内容になれば幸いです。
今回作成するToDoアプリ
以下が今回作成したToDoアプリの動作イメージです。
GitHubでソースコードも公開していますので、ぜひ参考にしてください。
ソースコードを見る
動作環境
- マシン : M1 MacBookAir
- Node.js:v20.14.0
- スマホ:iPhone14
React Nativeとは
React Nativeは、JavaScriptやTypeScriptを使って、iOSとAndroid向けのモバイルアプリを開発できるフレームワークです。同じコードで両プラットフォームに対応できるため、効率的な開発が可能です。
特徴:
- クロスプラットフォーム対応
- ネイティブアプリに近いパフォーマンス
- Reactの知識をそのまま活用可能
なお、競合フレームワークとしてFlutterがよく比較対象に挙げられます。
Expoとは
ExpoはReact Nativeを使った開発をさらに簡単にするためのツールセットです。通常、ネイティブアプリを開発するにはXcodeやAndroid Studioなどの環境構築が必要ですが、Expoを使用することでこれらを簡略化できます。また、デバッグやテストも効率的に行えるため、初心者から経験者まで広く利用されています。
環境構築
任意のディレクトリで以下コマンドを実行する
npx create-expo-app@latest
プロンプトが表示されたら以下のように入力する。
Ok to proceed? (y) # -> y
What is your app named? # -> react-native-todo
作成したプロジェクトディレクトリに移動する
cd react-native-todo
開発サーバーを起動する
npx expo start
スマホで表示されるQRコードを読み取ります。以下のようにアプリが起動すれば成功です!
ToDo作っていく
ディレクトリ構成
初期のディレクトリ構成
Expo Router
では、ファイルシステムベースのルーティングを採用しており、ディレクトリ構造が画面遷移やナビゲーションを定義します。
app
├── (tabs)
│ ├── _layout.tsx
│ ├── explore.tsx
│ └── index.tsx
├── +not-found.tsx
└── _layout.tsx
-
app/_layout.tsx
- アプリ全体のレイアウトを定義するファイル。
- すべてのページに共通するヘッダーやフッター、ナビゲーションの構造を設定します。
- このファイルがあることで、
app
内のルートが自動的にナビゲーションの対象になります。
-
app/(tabs)/_layout.tsx
- タブナビゲーションのレイアウトを定義するファイル。
-
(tabs)
という括弧で囲まれたディレクトリはグループルートと呼ばれ、タブ構成やルートのグループ化を表します。 - この中のすべての画面がタブとして扱われます。
-
app/(tabs)/index.tsx
- タブの1つ目の画面(デフォルトタブ)。
- URLで
/
にアクセスしたときに表示されるタブページです。
-
app/(tabs)/explore.tsx
- タブの2つ目の画面。
- URLで
/explore
にアクセスしたときに表示されます。
-
app/+not-found.tsx
- 存在しないルートにアクセスした場合に表示される404エラーページ。
-
+
プレフィックスが付いているファイルは、特殊な機能を持つページを定義します。
今回は以下のようにディレクトリを簡略化して進めます。
app
├── index.tsx # メインページ
├── +not-found.tsx # 404ページ
└── _layout.tsx # 単純化されたレイアウト
不要ファイルの削除
-
index.tsx
をapp
直下に移動
タブ構成のindex.tsx
を1ページのアプリのメインとして使います。 -
(tabs)
フォルダを削除
タブナビゲーションを全て削除します。 -
_layout.tsx
を単純化
ナビゲーションを1ページ用に変更します。
雛形を作成する
app/index.tsx
import React, { useState } from "react";
import { View, Text, TextInput, TouchableOpacity, FlatList, StyleSheet } from "react-native";
interface Task {
id: string;
text: string;
}
export default function HomeScreen() {
const [task, setTask] = useState<string>("");
const [tasks, setTasks] = useState<Task[]>([]);
const [editingTaskId, setEditingTaskId] = useState<string | null>(null);
const [editingText, setEditingText] = useState<string>("");
// タスクを追加する
const addTask = () => {
if (task.trim()) {
setTasks([...tasks, { id: Date.now().toString(), text: task }]);
setTask("");
}
};
// タスクを削除する
const deleteTask = (id: string) => {
setTasks(tasks.filter((t) => t.id !== id));
};
// タスクを編集状態にする
const startEditing = (id: string, currentText: string) => {
setEditingTaskId(id);
setEditingText(currentText);
};
// 編集内容を保存する
const saveEdit = () => {
setTasks(tasks.map((t) => (t.id === editingTaskId ? { ...t, text: editingText } : t)));
setEditingTaskId(null);
setEditingText("");
};
return (
<View style={styles.container}>
<Text style={styles.title}>Todo App</Text>
<View style={styles.inputContainer}>
<TextInput
style={styles.input}
placeholder="Add a new task..."
value={task}
onChangeText={setTask}
/>
<TouchableOpacity style={styles.addButton} onPress={addTask}>
<Text style={styles.addButtonText}>+</Text>
</TouchableOpacity>
</View>
<FlatList
data={tasks}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.taskContainer}>
{editingTaskId === item.id ? (
// 編集中のUI
<TextInput
style={[styles.input, { flex: 1, marginRight: 10 }]}
value={editingText}
onChangeText={setEditingText}
onSubmitEditing={saveEdit}
autoFocus
/>
) : (
// 通常表示のUI
<Text style={styles.taskText}>{item.text}</Text>
)}
{editingTaskId === item.id ? (
// 編集中の保存ボタン
<TouchableOpacity style={styles.saveButton} onPress={saveEdit}>
<Text style={styles.saveButtonText}>✔️</Text>
</TouchableOpacity>
) : (
// 通常時の編集と削除ボタン
<>
<TouchableOpacity
style={styles.editButton}
onPress={() => startEditing(item.id, item.text)}
>
<Text style={styles.editButtonText}>✏️</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => deleteTask(item.id)}>
<Text style={styles.deleteButton}>🗑️</Text>
</TouchableOpacity>
</>
)}
</View>
)}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 20,
backgroundColor: "#f5f5f5",
},
title: {
fontSize: 24,
fontWeight: "bold",
marginBottom: 20,
},
inputContainer: {
flexDirection: "row",
marginBottom: 20,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 5,
padding: 10,
backgroundColor: "#fff",
},
addButton: {
backgroundColor: "#007BFF",
marginLeft: 10,
padding: 10,
borderRadius: 5,
},
addButtonText: {
color: "#fff",
fontSize: 18,
},
taskContainer: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 10,
marginBottom: 10,
backgroundColor: "#fff",
borderRadius: 5,
borderWidth: 1,
borderColor: "#ccc",
},
taskText: {
fontSize: 16,
flex: 1,
},
deleteButton: {
fontSize: 18,
color: "#ff5c5c",
marginLeft: 10,
},
editButton: {
marginLeft: 10,
},
editButtonText: {
fontSize: 18,
color: "#007BFF",
},
saveButton: {
marginLeft: 10,
},
saveButtonText: {
fontSize: 18,
color: "#28a745",
},
});
app/_layout.tsx
import { Stack } from "expo-router";
export default function Layout() {
return (
<Stack>
<Stack.Screen name="index" options={{ title: "Todo App" }} />
</Stack>
);
}
app/+not-found.tsx
import React from "react";
import { View, Text, StyleSheet } from "react-native";
export default function NotFound() {
return (
<View style={styles.container}>
<Text style={styles.text}>404 - Page Not Found</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#f5f5f5",
},
text: {
fontSize: 20,
color: "#ff5c5c",
},
});
実証
再度、npx expo start
して反映されていれば成功です🎉
まとめ
いかがだったでしょうか。今回、React Nativeを使ったアプリ開発の流れを解説しました。初めて触れる技術でしたが、クロスプラットフォーム対応や開発効率の良さを少しずつ実感できました。まだまだReact Nativeの魅力を掘り下げる余地がありそうなので、今後も学習を続けていきたいと思います!
とりあえず入門しました〜。ということで、React Native教の皆様これからよろしくお願いします。
Discussion