最近React Nativeでアプリを作っていて、そのときに勉強したことのメモが散らばっていたのでまとめていこうと思う。
とりあえず今回の記事ではTodoを簡単に作っていく。
このTodoにReact Elements, React Natvigation, Redux sagaを追加したやつの記事も書いていく予定。
今回のソースは以下にあげてある
https://github.com/megaya/react_native_expo_todo_redux/tree/feature/todo-v1
今回はデザインは度外視している。次回にReact Elementsを導入して整えるので気にしない方向で
Expoとは
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でプロジェクトを作ってアプリで起動する
$ npm install --global expo-cli
$ expo init expo_todo_redux
=> blankを選択
$ cd expo_todo_redux
$ yarn start
(プロジェクトをブランクで作成すると上記のような画面が表示される)
yarn start
するとQRコードが表示される。
iOSかAndroidにExpoアプリをダウンロードして、このQRコードを読み込むとアプリが起動できる。
しかもホットリロードなのでソースコードを変更するだけで勝手にアプリが更新される。
タスクを追加する
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>や
`などはReact Nativeで用意されているコンポーネントだ。
公式サイトから使いたい画面の部品を探していろいろと使ってみるが早いと思う。
https://reactnative.dev/docs/components-and-apis.html
hooks(React.useState)について
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が更新されるようになる
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
配列をループさせてリストを表示する
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
で中身を表示させている。
render() {
return (
<View>
{
todos.map((item, i) => {
return(
<Text key={i}>
{item.text}
</Text>
)
})
}
</View>
);
}
他の配列を表示させる方法としてはmapでループさせる方法もある。
タスクを追加する処理
setTodos(oldTodos =>[...oldTodos, { id: id, text: text }])
タスクを追加するための処理は上記のようになっている。
既存の配列を...oldTodos
で展開して、配列のうしろに新しいオブジェクトを追加しているだけ。
単純にtodosにpushなどはしてはいけない。setTodos
を使用することで、Reactは変更があったことを検知して画面に描画している。
タスクを削除する処理の追加
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}
/>
タスクの削除の処理は追加の場合とほとんどと同じ。
setTodos(oldTodos => oldTodos.filter((oldTodo) => oldTodo.id !== item.id))
削除ボタンが押されたtodoのidと一致しないものを配列としてセットしているだけ
コンポーネントに分割する
今のままだとApp.jsファイルにすべてを書いてしまっているので、コンポーネントごとに分割していく。今のjsのフレームワークは基本的にコンポーネントごとにわける設計が中心になっていて、「ボタン」「フォーム」などのWebのパーツごとにファイルをわけるようになっている。
あとReact Nativeでは画面ごとにscreenというファイルを作り、それにコンポーネントを追加していく設計が多い。今回もそれに従っていいる。最終的なディレクトリは以下のようになる。
App.js
src/
├── components
│ ├── AddTodo.js
│ ├── TodoList.js
│ └── TodoListItem.js
└── screens
└── TodoScreen.js
screen
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
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
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
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を導入して見た目を整えたものを記事にする予定