[Expo][ReactNative]タスクアプリを作ってみた

# 挑戦って大事
Ionic+Angularでスマホアプリはプロジェクトでやってますが、それ以外で作る方法ってFlutterを少しかじったぐらいでして。。。
存在は知ってましたが、食わず嫌いで全然手つかずで来てしまったReactNativeを少し勉強がてらにいじってみた。

# とりあえず簡単なタスク管理という名のTODOアプリを作ってみた
## 作りたいものはこんな感じ
- とりあえず一覧画面と登録画面の2画面
- 登録データはローカルストレージに登録
※今回は削除とか編集とかはせず、あくまで登録だけ出来るやつを作る

## 開発PCはこんなん使ってるよ
- windows 11
- Node(nvm) 21

## とりあえずNodeにGlobalにインストール
- Expo CLI
```shell
npm install -g expo-cli
```
## じゃあプロジェクトを作ってみるよ
```shell
expo init TaskManagerApp
```
## 必要なライブラリとかを追加
一覧と登録画面と画面遷移が必要になるため navigation関係を追加、データをStorageに入れるためにreact-native-async-storageを追加
```shell
# 1行で書いてもいいよ
npm install @react-navigation/native
npm install @react-navigation/stack 
npm install @react-native-async-storage/async-storage
npm install react-native-screens 
npm install react-native-safe-area-context 
npm install react-native-gesture-handler 
npm install react-native-reanimated
```

### ちなみにファイル構造
TaskManagerApp/
│
├── App.tsx
├── types.ts
├── components/
│   ├── TaskItem.tsx
│   └── TaskList.tsx
├── screens/
│   ├── HomeScreen.tsx
│   └── TaskScreen.tsx
├── navigation/
│   └── AppNavigator.tsx   
└── storage/
　  └── StorageHelper.tsx

## 共通の型を定義ファイル
```typescript:types.ts
export interface Task {
  id: string;
  title: string;
}
```

## ストレージ関連のヘルパーファイル
```typescript:StorageHelper.ts
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Task } from '../types';

const TASKS_STORAGE_KEY = 'tasks';

export const saveTasks = async (tasks: Task[]): Promise<void> => {
  try {
    const jsonValue = JSON.stringify(tasks);
    await AsyncStorage.setItem(TASKS_STORAGE_KEY, jsonValue);
  } catch (e) {
    console.error('[Error]登録エラー', e);
  }
};

export const loadTasks = async (): Promise<Task[]> => {
  try {
    const jsonValue = await AsyncStorage.getItem(TASKS_STORAGE_KEY);
    return jsonValue != null ? JSON.parse(jsonValue) : [];
  } catch (e) {
    console.error('[Error]読み込みエラー', e);
    return [];
  }
};
```

## コンポーネント郡
### 一覧のタスク1行分のコンポーネント
```typescript:TaskItem.tsx
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import { Task } from '../types';

const TaskItem: React.FC<{ task: Task }> = ({ task }) => {
  return (
    <View style={styles.item}>
      <Text>{task.title}</Text>
    </View>
  );
};

const styles = StyleSheet.create({
  item: {
    padding: 20,
    borderBottomWidth: 1,
    borderBottomColor: '#ccc',
  },
});

export default TaskItem;
```


### 一覧部分のコンポーネント
```typescript:TaskList.tsx
import React from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Task } from '../types';
import TaskItem from './TaskItem';

interface TaskListProps {
  tasks: Task[];
}

const TaskList: React.FC<TaskListProps> = ({ tasks }) => {
  return (
    <View style={styles.container}>
      <FlatList
        data={tasks}
        renderItem={({ item }) => <TaskItem task={item} />}
        keyExtractor={item => item.id}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    width: '100%',
  },
});

export default TaskList;
```

## タスク一覧（ホーム）画面
```typescript:HomeScreen.tsx
import React, { useState, useEffect } from 'react';
import { View, Button, StyleSheet } from 'react-native';
import TaskList from '../components/TaskList';
import { Task } from '../types';
import { loadTasks, saveTasks } from '../storage/StorageHelper';

const HomeScreen: React.FC<{ navigation: any }> = ({ navigation }) => {
  const [tasks, setTasks] = useState<Task[]>([]);

  useEffect(() => {
    const loadStoredTasks = async () => {
      const storedTasks = await loadTasks();
      setTasks(storedTasks);
    };

    loadStoredTasks();
  }, []);

  useEffect(() => {
    saveTasks(tasks);
  }, [tasks]);

  const addTask = (newTask: Task) => {
    setTasks(prevTasks => [...prevTasks, newTask]);
  };

  return (
    <View style={styles.container}>
      <TaskList tasks={tasks} />
      <Button title="Add Task" onPress={() => navigation.navigate('Task', { addTask })} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default HomeScreen;
```

## 登録画面
```typescript:TaskScreen.tsx
import React, { useState } from 'react';
import { View, TextInput, Button, StyleSheet } from 'react-native';
import { Task } from '../types';

const TaskScreen: React.FC<{ navigation: any; route: any }> = ({ navigation, route }) => {
  const [task, setTask] = useState('');

  const addTask = () => {
    if (task.trim()) {
      const newTask: Task = { id: Math.random().toString(), title: task.trim() };
      route.params.addTask(newTask);
      navigation.goBack();
    }
  };

  return (
    <View style={styles.container}>
      <TextInput
        placeholder="New Task"
        value={task}
        onChangeText={setTask}
        style={styles.input}
      />
      <Button title="Add Task" onPress={addTask} />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  input: {
    width: '80%',
    padding: 10,
    marginBottom: 20,
    borderWidth: 1,
    borderColor: '#ccc',
  },
});

export default TaskScreen;
```

## ナビゲーション部分（画面遷移を制御する部分）
```typescript:AppNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
import TaskScreen from '../screens/TaskScreen';

type RootStackParamList = {
  Home: undefined;
  Task: undefined;
};

const Stack = createStackNavigator<RootStackParamList>();

function AppNavigator() {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen name="Home" component={HomeScreen} />
        <Stack.Screen name="Task" component={TaskScreen} />
      </Stack.Navigator>
    </NavigationContainer>
  );
}

export default AppNavigator;
```

## mainとなるのファイル
```typescript:App.tsx
import React from 'react';
import AppNavigator from './navigation/AppNavigator';

export default function App() {
  return <AppNavigator />;
}
```

# 実行の仕方
- AndroidでExpo Goをインストールする
- npm run start　で実行すると、QRコードがコンソールに表示される
- Expo GoでQRを読み取り動作を確認する

# 画面イメージ
## 一覧画面
![1000023027.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/996378/969a65fb-8cf5-7bfc-213d-232ebd11cb99.jpeg)
## 登録画面
![1000023028.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/996378/2a94ae7c-ba89-42ea-d50d-4c4be83c099d.jpeg)

# ね、簡単でしょ？
だいたい半日ぐらいでこんな感じで出来上がります。
とりあえず今回は【勉強がてらReactNativeとExpoを触ってみた】って言うレベルなので、細かい機能とか設定などについては特に何もやってません。

またExpoの機能なのか、実機で即確認ができるのがとてもありがたいですね。
マジでIonicとかFlutterみたいにAndroidStudioとか用意しなくても簡単に環境出来るし、アプリを入れなければいけない制約はありますが実機でそのまま確認が出来るのは本当にありがたい限りです。

ただ。。。ずっとAngularやVueばかりイジってきた自分からすると、Reactの書き方（コードのReturnがTagになってるやつ）が全然違和感ありまくりで気持ち悪いですね。。。
早く慣れることを願いますが。。。


# ちな、ソースを
gitにアカウント作って公開してみました。
[え～すけさんのGitHub](https://github.com/ASIL-E-Hatake/TaskManagerApp/tree/main)