[React Native] Todoアプリつくってみた

React Nativeの理解を深めるために、Todoアプリを実装しました。

![ReactNative_Todo.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/393193/dc2f6611-5db0-5350-35e1-68358ce8a3cf.jpeg)


# 機能
|- フォームから追加
|- リストから削除
|- JSONから初期データ簡易表示

# 環境
|- React Native + Expo(SDK36)
|- React Navigation 5x(2020.3現在)
|- TypeScript

[環境構築はこちらを参照](https://qiita.com/dai_designing/items/0cdba2113aca2cf8baf4)

CLIは以下選択
managed workflow
Blank(TypeScript)

# 初期データ格納
簡易API作成
ルート内にapi/todosList.jsonを追加

```json:api/todosList.json
[
    { 
        "id": 1,
        "title": "initial-title",
        "description": "initial-descript,initial-descript,initial-descript,initial-descript",
        "done": false
    },
    { 
        "id": 2,
        "title": "initial-title2",
        "description": "initial-descript,initial-descript,initial-descript,initial-descript",
        "done": false
    }
]
```

# App.tsxの編集

ひとまずファイルは分けずに1ファイルで書きます。

```tsx:App.tsx
import React, { FC, Fragment ,useState, useEffect } from 'react';
import { StyleSheet, View, Text, TextInput, TouchableOpacity, FlatList, Modal, SafeAreaView } from 'react-native';
// 簡易API
import todoList from './api/todosList.json';

// 型指定
// Todoタイプ定義
type Todo = {
  id: number;
  title: string;
  description: string;
  done: boolean;
}
// Modeタイプ定義
type Mode = 'list' | 'add';

// TypeScriptでのファンクションコンポーネントの型指定
const App:FC = () => {
 
  // 初期値（いらなければ取るかコメントアウト）
  const [ready, setReady] = useState(false);
  const getReady = () => {
    setTodos(todoList);
    setReady(true);
  }
  useEffect(() => {
    getReady();
  }, []);

  // モードチェンジ
  const [mode, setMode] = useState<Mode>('list');
  const changeMode = (mode: Mode) => {
    setMode(mode);
  }
  const handlePlus = () => {
    changeMode('add'); // modal表示
  }
  const handleCancel = () => {
    changeMode('list'); // リスト表示
  }

  // TODO追加
  const [todos, setTodos] = useState<Todo[]>([]);
  const addTodo = (todo: Todo) => {
    setTodos(todos => [...todos, todo]);
  }
  const handleAdd = () => {
    if(!title || !description) return;

    const newTodo: Todo = {
      id: todos.length === 0 ? 1 : todos[todos.length - 1].id + 1,
      title,
      description,
      done: false
    }
    addTodo(newTodo);
    changeMode('list');
  }

  // TODO入力フォーム初期値
  const [title, setTitle] = useState('');
  const [description, setDescription] = useState('');
  const resetInput = () => {
    setTitle('');
    setDescription('');
  }
  useEffect(() => {
    if(mode === 'list') {
      resetInput();
    }
  }, [mode]);

  // TODO削除
  const deleteTodo = (id: number) => {
    // filterメソッド: 配列の値を抽出するメソッド => [応用]選択されたIDが違うものだけを抽出するので同じIDを持つ内容は削除される
    setTodos(todos => todos.filter(todo => todo.id !== id))
  }
  const handleDelete = (id: number) =>{
    deleteTodo(id);
  }

  // 描画部分
  return (
    <Fragment>
      <SafeAreaView style={styles.container}>
        <View>
          <TouchableOpacity onPress={() => handlePlus()}>
            <Text style={ styles.plus }>+ TODO ADD</Text>
          </TouchableOpacity>
        </View>

        <View style={styles.todo_wrapper}>
          <FlatList
            data={todos}
            renderItem={({ item: todo }) => {
              return (
                <View style={styles.todo_container}>
                  <Text numberOfLines={2} style={styles.todo_title}>
                    { todo.title }: { todo.description }
                  </Text>
                  <TouchableOpacity onPress={ () => handleDelete(todo.id) }>
                    <Text style={ styles.delete }>Delete</Text>
                  </TouchableOpacity>
                </View>
              );
            }}
            keyExtractor={(_, index) => index.toString()}
          />
        </View>
      </SafeAreaView>

      <Modal visible={ mode === 'add'} animationType={ 'slide' }>
        <View style={ styles.modal }>
          <View style={ styles.textinput_frame }>
            <TextInput
              placeholder={'Title'}
              value={ title }
              onChangeText={ text => setTitle(text) }
              style={ styles.textinput }
            />
            <TextInput
              placeholder={'Description'}
              value={ description }
              onChangeText={ text => setDescription(text) }
              style={ styles.textinput }
            />
          </View>
          <View style={ styles.button }>
            <TouchableOpacity onPress={() => handleAdd()}>
              <Text style={ styles.add }>Add</Text>
            </TouchableOpacity>
            <TouchableOpacity onPress={() => handleCancel()}>
              <Text style={ styles.cancel }>Cancel</Text>
            </TouchableOpacity>
          </View>
        </View>
      </Modal>
    </Fragment>
  );

}

const styles = StyleSheet.create({
  container: {
    flex: 1,
  },
  modal: {
    justifyContent: 'center', 
    alignItems: 'center', 
    backgroundColor: "#fff", 
    height: 400,
  },
  todo_wrapper: {
    marginTop: 25,
  },
  todo_container: {
    flex: 1,
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    marginBottom: 1,
    paddingLeft: 15,
    backgroundColor: '#f5f5f5',
  },
  todo_title: {
    width: 300,
    fontSize: 15,
    lineHeight: 20,
    textAlign: 'left',
  },
  plus: {
    fontSize: 20,
    textAlign: 'center',
    color: '#4169e1',
    marginTop: 15,
    paddingLeft: 15,
  },
  add: {
    fontSize: 15,
    textAlign: 'center',
    color: '#fff',
    width: 250,
    marginTop: 20,
    marginRight: 5,
    padding: 10,
    backgroundColor: '#4169e1',
  },
  cancel: {
    fontSize: 15,
    textAlign: 'center',
    color: '#fff',
    width: '100%',
    marginTop: 20,
    marginLeft: 5,
    padding: 10,
    backgroundColor: '#ccc',
  },
  textinput_frame: {
    width: '100%',
    marginBottom: 25,
  },
  textinput: {
    fontSize: 18,
    borderColor: '#ccc',
    borderBottomWidth: 1,
    padding: 25,
  },
  button: {
    flexDirection: 'row',
  },
  delete: {
    fontSize: 14,
    padding: 30,
    color: '#fff',
    backgroundColor: '#dc143c',
  }
});

export default App;
```

ここまでで機能自体の実装は終わりです。
ただ永続保存など仕組みは組んでないので、
自分の技術向上も含めて調査が必要です。

# ナビゲーション

2020年3月現在のバージョン
React Navigation 5xインストール
@react-navigation/native@5.0.9
@react-navigation/stack@5.1.1

```console
$ npm install @react-navigation/native
$ npm install @react-navigation/stack
```

さらにインストール

```console
$ expo install react-native-gesture-handler react-native-reanimated react-native-screens react-native-safe-area-context @react-native-community/masked-view
```

```tsx:screens/HomeScreen.tsx
// App.tsxからまず全コピー
// その後一部変更

// ①階層変更
// import todoList from './api/todosList.json';
import todoList from '../api/todosList.json';

// ②関数名変更
// const App:FC = () => {
const HomeScreen:FC = () => {

// ③exportの方も変更
// export default App;
export default HomeScreen;
```

```js:navigation/AppNavigator.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';

const Stack = createStackNavigator();

export default AppNavigator = () => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={ HomeScreen }
          options={{ title: 'TODO' }}
          // options={{ headerShown: false }}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
}
```

```tsx:App.tsx
import React, { FC } from 'react';
import AppNavigator from './navigation/AppNavigator';

const App:FC = () => {
    return <AppNavigator />
}

export default App;
```

# ボトムタブナビゲーター関連（5x）

React Navigation 5x

```console
$ npm install @react-navigation/bottom-tabs
```

```js:navigation/AppNavigator.js
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createStackNavigator } from '@react-navigation/stack';
import HomeScreen from '../screens/HomeScreen';
// 追加
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { FontAwesome } from '@expo/vector-icons';

const Stack = createStackNavigator();
// 追加
const Tab = createBottomTabNavigator();

// 追加
const HomeStack = () => {
    return (
      <Stack.Navigator>
        <Stack.Screen
          name="Home"
          component={ HomeScreen }
          options={{ headerShown: false }}
        />
      </Stack.Navigator>
    );
  }
  
  // 追加
  const screenOption = ({route}) => ({
    tabBarIcon: ({ color, size }) => {
      let iconName;
  
      if (route.name === 'Home') {
        iconName = 'home';
      }
  
      return <FontAwesome name={iconName} size={size} color={color} />;
    },
  });

export default AppNavigator = () => {
  return (
    <NavigationContainer>
        {/* 追加 screenOptions={ screenOption } */}
        <Tab.Navigator screenOptions={ screenOption }>
            <Tab.Screen name="Home" component={ HomeStack } />
        </Tab.Navigator>
    </NavigationContainer>
  );
}
```

# テスト配布

```console
$ expo publish
```
（注）実機確認したところTextInputで入力した文字が白くなってしまう現象が発生（原因調査中）

[ビルド関連はこちらを参照](https://qiita.com/dai_designing/items/74f00f62cf1c215418d0)

# 今後の目標
|- TextInputで入力した文字が白くなってしまう現象の解消
|- TextInputから入力した内容の永続保存
|- ✅ボックス（TODO完了）の実装