React Native + Expoでtodoアプリを作る


最近React Nativeでアプリを作っていて、そのときに勉強したことのメモが散らばっていたのでまとめていこうと思う。

とりあえず今回の記事ではTodoを簡単に作っていく。
このTodoにReact Elements, React Natvigation, Redux sagaを追加したやつの記事も書いていく予定。

![スクリーンショット 2020-03-09 10.55.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/b090d224-af2a-5531-498c-ce9768decd38.png "スクリーンショット 2020-03-09 10.55.45.png")


今回のソースは以下にあげてある
https://github.com/megaya/react_native_expo_todo_redux/tree/feature/todo-v1


今回はデザインは度外視している。次回にReact Elementsを導入して整えるので気にしない方向で

## Expoとは

https://expo.io/

React NativeはJsでiOSやAndroidアプリが作成できるフレームワークなのだけれど、ネイティブの知識がある程度は必要になってくる。アプリへのビルドなども自前で行う必要がある。

Expoはそういったことをすべて自動でやってくれるサービスだ。ネイティブコードを一切書かずに本当の意味でjsの知識だけでiOS/Androidアプリが作成できる。さらにAppleの審査なしにリリースができるという特徴もある。

### Expoでできないこと

難点としてはexpoでネイティブのコードを固定しているので、できることが限らているという点だ。ネイティブ機能をふんだんに使ったアプリをExpoで作るのは難しい。例えばお財布ケータイなどのデバイス特有の機能はExpoで使うことはできない（厳密にはやろうと思えばできる）。あと現状だとGoogle Analyticsも使えない。GAについてはissueやプルリクが出されているので、そのうちマージされるはず。

あとReactのライブラリもExpoだと使用できないものがあるので注意が必要。

### Eject

ただしexpoで使えない機能を使うようにすることもできる。Ejectという機能だ。Expo プロジェクトを通常の React Native プロジェクトに変換し、 Xcode や Android Studio を使ってビルドできるようになるのだ。

以前はEjectするためにExpoから抜け出さないといけなかった。しかし、2019年7月よりExpo Bare Workflowという機能が追加されて、一度EjectしてもExpo clinetで動かせるようになった。

うーん、便利だ。ネイティブを知らない人はEjectをなるべく使わなければいいし、ネイティブ知識がある人は機能が足りなくなったときにEjectすればいいと思う。


## Expoでプロジェクトを作ってアプリで起動する


```console
$ npm install --global expo-cli

$ expo init expo_todo_redux
=> blankを選択

$ cd expo_todo_redux

$ yarn start
```

![スクリーンショット 2020-03-09 10.54.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/6d6a2f4d-6def-f466-6b80-ab35c0d9d5f2.png "スクリーンショット 2020-03-09 10.54.08.png")
(プロジェクトをブランクで作成すると上記のような画面が表示される）


`yarn start`するとQRコードが表示される。
iOSかAndroidにExpoアプリをダウンロードして、このQRコードを読み込むとアプリが起動できる。
しかもホットリロードなのでソースコードを変更するだけで勝手にアプリが更新される。

## タスクを追加する

![スクリーンショット 2020-03-09 10.58.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/17de8f49-5893-e96b-26a7-264e215e9a09.png "スクリーンショット 2020-03-09 10.58.12.png")


```jsx:App.js
function Item({ text }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
    </View>
  );
}

export default function App() {
  const [text, onChangeText] = React.useState("");
  const [todos, setTodos] = React.useState([]);
  const [id, setID] = React.useState(1);

  return (
    <SafeAreaView style={styles.container}>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={t => onChangeText(t)}
        value={text}
      />
      <Button
        title="Press me"
        onPress={() => {
          onChangeText("")
          setTodos(oldTodos =>[...oldTodos, { id: id, text: text }])
          setID(id + 1)
        }}
      />
      <FlatList
        data={todos}
        renderItem={({ item }) => <Item text={item.text} />}
        keyExtractor={item => item.id}
      />
    </SafeAreaView>
  );
}
```

まずはテキストボックスで入れた文字を一覧で表示するところまで。
`<Button>や`<FlatList>`などはReact Nativeで用意されているコンポーネントだ。
公式サイトから使いたい画面の部品を探していろいろと使ってみるが早いと思う。
https://reactnative.dev/docs/components-and-apis.html


### hooks（React.useState）について

```jsx
export default function App() {
  const [value, onChangeText] = React.useState('Hello World');

  return (
    <View style={styles.container}>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={text => onChangeText(text)}
        value={value}
      />
      <Text>{ value }</Text>
    </View>
  );
}
```

`const [value, onChangeText] = React.useState('hogehoge')`という風に書くと、stateのような振る舞いを持つことができる。

* `useState('hogehoge')`のhogehogeの値は初期値で好きなものを入れられる
	* 配列やオブジェクトなど好きな値が入れられる
* 戻り値でuseStateでセットした値(value)と、valueをセットするための関数が作成される
	* onChangeTextはsetStateと同じような振る舞いをするため、値を変更するとDOMが更新されるようになる

```jsx
export default class App extends React.Component {

  constructor(props) {
    super(props);
    this.state = { value: "Hello World" };
  }

  onChangeText(text) {
    this.setState({ value: text });
  }

  render() {
    return (
      <View style={styles.container}>
        <TextInput
          style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
          onChangeText={text => this.onChangeText(text)}
          value={this.state.value}
        />
        <Text>{ this.state.value }</Text>
      </View>
    );
  }
}
```

hooksを使用しないで同じようなコードを書くと上記のようになる。
classとしてコンポーネントを作成してstateで状態を保持する必要がある。これでもいいのだけれどアプリが巨大になってくるとコードが複雑になっていく。hooksを使っておけば再利用もしやすいし、コードを薄く保つことできる。

ただ何でもかんでもhooksでやればいいってもんじゃないから、そのあたりは使い分けを考えたほうがいい。

hooks自体はReactの機能なので詳しくは公式サイトを見てください
[フック早わかり – React](https://ja.reactjs.org/docs/hooks-overview.html)


### 配列をループさせてリストを表示する

```jsx
function Item({ text }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
    </View>
  );
}

// ~省略~

  <FlatList
    data={todos}
    renderItem={({ item }) => <Item text={item.text} />}
    keyExtractor={item => item.id}
  />

```

FlatListでtodosをループさせて`Item`で中身を表示させている。

[FlatList · React Native](https://reactnative.dev/docs/flatlist)


```jsx
  render() {
    return (
      <View>
        {
          todos.map((item, i) => {
            return(
              <Text key={i}>
                {item.text}
              </Text>
            )
          })
        }
      </View>
    );
   }
```

他の配列を表示させる方法としてはmapでループさせる方法もある。


### タスクを追加する処理

```js
setTodos(oldTodos =>[...oldTodos, { id: id, text: text }])
```

タスクを追加するための処理は上記のようになっている。
既存の配列を`...oldTodos`で展開して、配列のうしろに新しいオブジェクトを追加しているだけ。

単純にtodosにpushなどはしてはいけない。`setTodos`を使用することで、Reactは変更があったことを検知して画面に描画している。

## タスクを削除する処理の追加


```jsx
function Item({ text, onPressDelete }) {
  return (
    <View style={styles.item}>
      <Text style={styles.title}>{text}</Text>
      <Button
        title="削除"
        onPress={onPressDelete}
      />
    </View>
  );
}


// ~~省略~~

<FlatList
  data={todos}
  renderItem={({ item }) => <Item text={item.text} onPressDelete={() => setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id) ) } />}
  keyExtractor={item => item .id}
/>
```

タスクの削除の処理は追加の場合とほとんどと同じ。

```js
setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id))
```

削除ボタンが押されたtodoのidと一致しないものを配列としてセットしているだけ

## コンポーネントに分割する

![スクリーンショット 2020-03-09 11.07.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59008/a76e5686-f85d-2f97-b996-ca73692fd650.png "スクリーンショット 2020-03-09 11.07.59.png")

今のままだとApp.jsファイルにすべてを書いてしまっているので、コンポーネントごとに分割していく。今のjsのフレームワークは基本的にコンポーネントごとにわける設計が中心になっていて、「ボタン」「フォーム」などのWebのパーツごとにファイルをわけるようになっている。

あとReact Nativeでは画面ごとにscreenというファイルを作り、それにコンポーネントを追加していく設計が多い。今回もそれに従っていいる。最終的なディレクトリは以下のようになる。


```
App.js
src/
├── components
│   ├── AddTodo.js
│   ├── TodoList.js
│   └── TodoListItem.js
└── screens
    └── TodoScreen.js
```

### screen

```jsx
import React from 'react';
import { StyleSheet, SafeAreaView } from 'react-native';
import AddTodo from '../components/AddTodo'
import TodoList from '../components/TodoList'

export default function TotoList() {
  const [todos, setTodos] = React.useState([]);
  const [id, setID] = React.useState(1);

  return (
    <SafeAreaView style={styles.container}>
      <AddTodo setTodos={(text) => {
        setTodos(oldTodos => [...oldTodos, { id: id, text: text }])
        setID(id + 1)
      }}
      />
      <TodoList todos={todos} setTodos={setTodos} />
    </SafeAreaView>
  );
}

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

```

### compornent

#### ./components/TodoList.js

```jsx
import React from 'react';
import { FlatList } from 'react-native';
import TodoListItem from './TodoListItem'

export default function TotoList({todos, setTodos}) {
  return (
    <FlatList
      data={todos}
      renderItem={({ item }) => <TodoListItem text={item.text} onPressDelete={() => setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id) ) } />}
      keyExtractor={item => item .id}
    />
  );
}

```

#### ./components/TodoItem.js

```jsx
import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';

export default function TodoListItem({ text, onPressDelete }) {
  return (
    <View style={styles.item}>
      <Text>{text}</Text>
      <Button
        title="削除"
        onPress={onPressDelete}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
});
```

#### ./components/AddTodo.js

```jsx
import React from 'react';
import { View, TextInput, Button } from 'react-native';

export default function AddTodo({setTodos}) {
  const [text, onChangeText] = React.useState("");

  return (
    <View>
      <TextInput
        style={{ height: 40, borderColor: 'gray', borderWidth: 1 }}
        onChangeText={t => onChangeText(t)}
        value={text}
      />
      <Button
        title="Press me"
        onPress={() => { setTodos(text); onChangeText("") }}
      />
    </View>
  );
}

```

## 終わり

次はReact Elementsを導入して見た目を整えたものを記事にする予定