React Hooks で Redux を使うための情報はパラパラありますが、一通り動作する小さなコードがなかなか見つからなかったため、こちらにまとめました。
※ 想定読者は「Redux は React などと一緒に状態管理に使うものらしい。Flux の図もなんとなく見たことある。でもコードは書いたことない」くらいの方です
※ サンプルコードは React Native + Expo、TypeScript です
※ 動作確認は expo start
で実施したまでです
※ 筆者は React 超初心者です
ソースコードは こちら です。
環境
$ node -v
v12.3.1
$ yarn -v
1.16.0
$ expo --version
3.4.1
その他のバージョンは package.json を参照ください。
準備
Expo プロジェクトの初期化
expo init
でプロジェクトを初期化しました。
テンプレートとしては blank (TypeScript) を、パッケージマネージャとしては Yarn を選択しました。
Redux のインストール
$ yarn add redux react-redux
$ yarn add --dev @types/react-redux
初期化時点の App.tsx
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
export default function App() {
return (
<View style={styles.container}>
<Text>Open up App.tsx to start working on your app!</Text>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#fff',
alignItems: 'center',
justifyContent: 'center',
},
});
これで実装の準備が整いました。
ミニマムコードの実装
合計で 3 つのファイルだけ新規作成・編集します。
内容は以下の通りです。
- Redux の action、reducer などを src/store.ts の 1 ファイルに全て書く
- Todo を入力・表示するコンポーネントを src/Todo.tsx に作る
- App.tsx で Redux の store を設定する
Redux の action、reducer などの実装
import { createStore } from "redux"
// actions
const ADD_TODO_ACTION = 'ADD_TODO'
// action creaters
export function addTodo(todo: string) {
return {
type: ADD_TODO_ACTION,
payload: {
todo
}
}
}
// init state
const initState = {
todos: []
}
// reducer
export const todoReducer = (state = initState, action) => {
switch (action.type) {
case ADD_TODO_ACTION:
return {
todos: [...state.todos, action.payload.todo]
}
default:
return state
}
}
// store
export const store = createStore(todoReducer)
各要素はざっくり言うと以下のようになっています。
- actions ... store に保存した値への操作の種類を定義
- action creaters ... アクションの実態を生成する関数
- init state ... store に保存する状態の初期値
- reducers ... 各アクションに対する実際の処理を記述
Todo コンポーネントの作成
import React, { useState } from "react";
import { StyleSheet, Text, View, TextInput, Button } from "react-native";
import { useDispatch, useSelector } from "react-redux";
import { addTodo } from "./../src/store";
export default function Todo() {
const [todo, setTodo] = useState("");
const dispatch = useDispatch();
const todos = useSelector<any, Array<string>>(state => state.todos);
const addTodoOnPress = () => {
if (todo === "") {
return;
}
dispatch(addTodo(todo));
setTodo("");
};
return (
<View style={styles.container}>
<Text>TODO 入力</Text>
<TextInput value={todo} onChangeText={t => setTodo(t)}></TextInput>
<Button title="保存" onPress={addTodoOnPress} />
{todos.map((t, i) => (
<View key={i}>
<Text>{t}</Text>
</View>
))}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
});
使っている要素をざっくり説明すると ...
- useState ... コンポーネント内に状態を持つための関数。この戻り値の todo が TextInput と双方向バインドされるかんじ
- useDispatch ... Redux の action を実行 (dispatch) するための準備。Button の onPress で
dispatch(addTodo(todo))
という処理を実行し、store に addTodo という action creater の結果を実行 (dispatch) する - useSelector ... Redux の store から値を取り出している。
todo.map ...
の部分で一覧を画面に表示する
App.tsx の書き換え
import React from "react";
import { Provider } from "react-redux";
import { store } from "./src/store";
import Todo from "./src/Todo";
export default function App() {
return (
<Provider store={store}>
<Todo />
</Provider>
);
}
全体を react-redux の Provider
で囲み、store を設定してあげるだけです。
混乱したところ
mapStateToProps、matchDispatchToProps、connect
Native Base のドキュメントの「Basic Redux Counter Example」などでは mapStateToProps、matchDispatchToProps、connect などがコンポーネントと一緒に書かれているが、これは必要なのか最初はよく分かりませんでした。
結論としては不要でした。
おわりに
最初は何をどうすればいいのか全然分からなかったので、こうしてまとめられて満足です。
React Hooks など、React の記述はどんどんアップデートされており、周辺エコシステムも充実しているため、React を一から学ぶという状態では何が一番今風の書き方なのかなかなか分かりませんでした。
もっと調べていくと、さらにブラッシュアップされるのかもしれません。。