📱

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
  1. app/_layout.tsx

    • アプリ全体のレイアウトを定義するファイル。
    • すべてのページに共通するヘッダーやフッター、ナビゲーションの構造を設定します。
    • このファイルがあることで、app内のルートが自動的にナビゲーションの対象になります。
  2. app/(tabs)/_layout.tsx

    • タブナビゲーションのレイアウトを定義するファイル。
    • (tabs)という括弧で囲まれたディレクトリはグループルートと呼ばれ、タブ構成やルートのグループ化を表します。
    • この中のすべての画面がタブとして扱われます。
  3. app/(tabs)/index.tsx

    • タブの1つ目の画面(デフォルトタブ)。
    • URLで/にアクセスしたときに表示されるタブページです。
  4. app/(tabs)/explore.tsx

    • タブの2つ目の画面。
    • URLで/exploreにアクセスしたときに表示されます。
  5. app/+not-found.tsx

    • 存在しないルートにアクセスした場合に表示される404エラーページ。
    • +プレフィックスが付いているファイルは、特殊な機能を持つページを定義します。

今回は以下のようにディレクトリを簡略化して進めます。

app
├── index.tsx         # メインページ
├── +not-found.tsx    # 404ページ
└── _layout.tsx       # 単純化されたレイアウト

不要ファイルの削除

  1. index.tsxapp直下に移動
    タブ構成のindex.tsxを1ページのアプリのメインとして使います。

  2. (tabs)フォルダを削除
    タブナビゲーションを全て削除します。

  3. _layout.tsxを単純化
    ナビゲーションを1ページ用に変更します。

雛形を作成する

app/index.tsx
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
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
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