Redux について(実践編)
2020-05-27
前回の記事で Redux の概念について説明しました。
この記事では Redux の概念をもとに、React.js を使って 実際のコードではどのように表現していくのかみていきます。
React Project 作成
ローカルに Create React App を使って React をインストールするため、 ターミナルで以下のコマンドを実行します。
npx create-react-app my-redux-app
インストールが終わったら、プロジェクトのディレクトリに移動して yarn start
を実行し、
アプリが正常に動くか確認します。
cd my-redux-app
yarn start
http://localhost:3000 にアクセスして以下のように表示されれば正常にインストールされています。
create-react-app
を使ってインストールすると、制作が始めやすいように複数のファイルが用意されています。
ただし今回はこのファイルを使わないので /src
ディレクトリにあるファイルすべて削除します。
cd src
git rm *
cd ..
mkdir src
React のインストールが終わったので、Redux による State 管理を追加していきます。
概念編でみてきた順番で実践編もみていきたいと思います。
State
Todo アプリで実装したい State の形は以下のようなオブジェクトです。
{
todos:[
{
text: "筋トレをする",
completed: false
},
{
text: "買物に行く",
completed: false
}
]
}
上記はすでにタスクが追加された状態です。では State の初期状態はというとタスクが無い状態になります。
{
todos: []
}
こちらが State の初期の状態です。この状態をファイルに記述しましょう。
src/initialState.js
ファイルを作成し、以下のコードを記述します。
export const initialState = {
todos: []
};
つづいて Action をみていきます。
Action
Action はやりたことを定義します。ここでは「タスクを追加する」という Action を実装していきます。
Action はオブジェクトで表現します。
{
type: 'ADD_TODO',
text: '筋トレをする'
}
type
が「やりたいこと」で、text
がタスクを追加するときに必要な「タスク名」です。
実装する際は Action Creator というものを使います。 src/actions.js
ファイルを作成し、以下のコードを記述します。
export const addTodo = text => {
return {
type: 'ADD_TODO',
text // `text: text` の省略記法
}
};
返しているものは { type: 'ADD_TODO', text: text' }
という Action です。Action を作って返しているのでまさに Action Creator ですね。
また Action の type
は Reducer の Switch 文でも使うので、使い回しできるように別途定義しておきます。
export const ADD_TODO = 'ADD_TODO';
export const addTodo = text => {
return {
type: ADD_TODO,
text
}
};
State と Action を実装したので、次は Reducer をみていきます。
Reducer
Reducer は State の値を変更する処理を定義するところです。
現在の State と Action を受け取って、新しい State を返す関数です。式で表すと
(nowState, action) => newState;
こんな関数になります。
Todo アプリの「タスクを追加する」ときにどのように実装するか、みていきましょう。 一番最初の形は以下のようになります。
src/reducers.js
ファイルを作成し、以下のコードを記述します。
const todoApp = (state, action) => {
return state;
};
上のコードでは State が変わっていませんが、State と Action を受け取って State を返すという形を作りました。
一番はじめの State は、State の章で定義した initialState
になります。
引数 state
のデフォルト値として initialState
を設定しておきましょう。
const initialState = {
todo: []
};
const todoApp = (state = initailState, action) => {
return state;
};
次に「タスクを追加する」ときの State を変化させるための処理を追加していきます。
「タスクを追加する」ときは action で受け取った text
名のタスクを todos
の配列に追加することになります。
コードにすると以下のようになります。
const initialState = {
todo: []
};
const todoApp = (state = initailState, action) => {
return {
...state,
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
};
};
ここでは Object の spread 構文でのマージと配列の spred 構文でのマージを使って、新しい State を返しています。
最後に Action は複数存在するので action.type によって処理を分岐させます。
また action.type 名を使い回せるように src/actions.js
で変数として定義しているので、そちらを使います。
import { ADD_TODO } from './actions';
const initialState = {
todo: []
};
const todoApp = (state = initialState, action) => {
switch (action.type) {
case ADD_TODO:
return {
...state,
todos: [
{
...state.todos,
text: action.text,
completed: false
}
]
};
default:
return state;
}
};
条件分岐に Switch 文を使いました。default の場合はそのまま state を返すようにしています。
ここまでで State、Action、Reducer についてみてきました。次は Store についてみていきましょう。
Store
Store は今までみてきた State
Action
Reducer
をまとめるものになります。
Store を使うために Redux をインストールします。
yarn add redux
Store は Redux の createStore()
メソッドに Reducer を渡して作成します。
src/store.js
ファイルを作成し、以下のコードを記述します。
import { createStore } from 'redux';
import { todoApp } from './reducers';
const store = createStore(todoApp);
これで Redux による State 管理に必要なことは実装できました。
Dispatch
Store のデータを呼び出せるか確認してみましょう。
src/index.js
ファイルを作成し、以下のコードを記述します。
import React from 'react';
import { render } from 'react-dom';
import { store } from "./store";
console.log(store.getState());
render(
<div />,
document.getElementById('root')
);
View に関してはまだ実装していません。仮に空の div
があるだけです。
5行目に console.log(store.getState())
で現在の State は
この状態でターミナルから以下のコマンドを実行して、ブラウザのコンソール画面で出力を確認します。
yarn start
ブラウザのコンソールを確認してみると、
todos が空の配列で取得できているのがわかります。
続いて State を更新する、「タスクを追加する」を確認してみましょう。
src/index.js
ファイルの内容を以下のように変更します。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions"; // action をインポート
import { store } from "./store";
console.log(store.getState()); // 初期の State を確認
store.dispatch(addTodo('筋トレをする')); // タスクを追加
console.log(store.getState()); // 追加後の State を確認
render(
<div />,
document.getElementById('root')
);
store.dispatch(addTodo('筋トレをする'))
でタスクを追加しました。
その結果、State が更新されて新しい State にタスクが追加されていることがわかります。
State が更新されるたびに getState()
で確認するのは面倒なので
更新を監視するために subscribe()
を追加します。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
render(
<div />,
document.getElementById('root')
);
unscribe();
State が更新されると subscribe() が実行されます。
そのため dispatch()
後に再度 console.log(store.getState())
を行わなくても
コンソールに最新の State が表示されます。
最後の行で subscribe()
メソッドの返り値である unscribe()
を実行して subscribe を解除しています。
Store を使って State を更新することができるようになりました。
ここから React のコンポーネント内で Redux を使えるようにしていきます。
React
追加したタスクが ul > li で表示させるようにします。
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
render(
<ul>
<li>筋トレをする</li>
<li>買物に行く</li>
</ul>,
document.getElementById('root')
);
unscribe();
コンポーネントに直接タスクを記述するとこんな感じになります。
わかりやすく App
コンポーネントに分けます。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
const App = () => (
<ul>
<li>筋トレをする</li>
<li>買物に行く</li>
</ul>
);
render(
<App />, // `App` コンポーネントに変更
document.getElementById('root')
);
unscribe();
さらに
- タスクのリストコンポーネント
- ひとつのタスクコンポーネント
に分けます。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
const Todo = ({ text }) => (
<li>
{text}
</li>
);
const TodoList = () => (
<ul>
<Todo text="筋トレをする" /> // Todo コンポーネントに変更
<Todo text="買物に行く" /> // Todo コンポーネントに変更
</ul>
);
const App = () => (
<TodoList />
);
render(
<App />,
document.getElementById('root')
);
unscribe();
タスクの名前を text
プロパティで渡し、 Todo
コンポーネントで分割代入(Destructing assignment)を使って受け取っています。
タスクのデータがハードコーディングされたままなので、変数 initialState に保存して map()
メソッドで展開します。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
const initialState = {
todos: [
{
text: "筋トレをする",
completed: false
},
{
text: "買物に行く",
completed: false
}
]
};
const Todo = ({ text }) => (
<li>
{text}
</li>
);
const TodoList = ({ todos }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={index} text={todo.text} />
))}
</ul>
);
const App = () => (
<TodoList todos={initialState.todos} />
);
render(
<App />,
document.getElementById('root')
);
unscribe();
TodoList
コンポーネント内で Todo
コンポーネントをループ処理で作成しています。
複数のコンポーネントがひとつのファイルに混在しているので、見やすくするためのそれぞれのコンポーネントを個別のファイルにします。
Todo.js
ファイルを作成し、以下のコードを記述します。
import React from "react";
export const Todo = ({ text }) => (
<li>
{text}
</li>
);
TodoList.js
ファイルを作成し、以下のコードを追加します。
import React from "react";
import { Todo } from "./Todo";
export const TodoList = ({ todos }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={index} text={todo.text} />
))}
</ul>
);
App.js
ファイルを作成し、以下のコードを記述します。
import React from "react";
import { TodoList } from "./TodoList";
const initialState = {
todos: [
{
text: "筋トレをする",
completed: false
},
{
text: "買物に行く",
completed: false
}
]
};
export const App = () => (
<TodoList todos={initialState.todos} />
);
Todo.js
、 TodoList.js
と App.js
に分けたので元の src/index.js
ファイルは以下のようになります。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
import { App } from "./components/App";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
render(
<App />,
document.getElementById('root')
);
unscribe();
ここから App.js
内にある initialState を Store の State の値を使って表示できるように react-redux
を使って変更していきます。
React With Redux
react-redux
モジュールをインストールします。
yarn add react-redux
Store を React コンポーネントで使えるようにするために、
React Redux の Provider
コンポーネントで <App />
を囲んで store
を渡します。
src/index.js
ファイルを以下のように変更します。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
// React Redux から `Provider` をインポート
import { Provider } from "react-redux";
import { App } from "./components/App";
console.log(store.getState());
const unscribe = store.subscribe(() => console.log(store.getState()));
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
render(
// <App /> を <Provider で囲む
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
unscribe();
これで Store が使えるようになったので、
State を使いたい TodoList
コンポーネントに紐づけましょう。
直接 TodoList
に紐づけてしまうと View とロジックが混ざってしまい見づらいので TodoListContainer
コンポーネントを作り、このコンポーネントに
Store を紐づけます。
コンポーネントと Store を紐づけるには React Redux の connect()
を使います。
TodoListContainer.js
を作成し、以下のコードを記述します。
import { connect } from "react-redux";
import { TodoList } from "../components/TodoList";
export const TodoListContainer = connect()(TodoList);
ここに Store データをどのような形でコンポーネントのプロパティと紐づけるかを決める必要があります。
mapStateToProps()
メソッドを使って定義します。
TodoListContainer.js
内を以下のように変更します。
import { connect } from "react-redux";
import { TodoList } from "../components/TodoList";
const mapStateToProps = state => {
console.log(state);
return {
todos: state.todos
}
};
export const TodoListContainer = connect(mapStateToProps)(TodoList);
App
コンポーネントで使用している TodoList
を上記の TodoListContainer
に変えるため、 App
コンポーネントを以下のように変更します。
import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";
export const App = () => (
<TodoListContainer />
);
これで Store の State を表示できるようになったので、今までダミーで使用していた initialState
は必要なくなりました。
initialState
を削除してもタスクが表示されています。
index.js
ファイルの13行目の subscrib()
メソッドで表示している console.log と TodoListContainer.js
ファイルの5行目で表示している console.log の結果が同じであることも確認できます。
これで React コンポーネントに Store のデータを表示できるようになったので、index.js
ファイルにある getState()
と subscribe()
メソッドを削除します。
src/index.js
を以下のように変更します。
import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";
import { Provider } from "react-redux";
import { App } from "./components/App";
store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
続いてアプリ内でタスクを追加できるようにしていきましょう。
まずタスクを追加するためのテキスト入力フィールドと追加ボタンがあるコンポーネントを作成します。
AddTodo.js
ファイルを作成し、以下のコードを記述します。
import React from "react";
export const AddTodo = () => (
<form>
<input
name="taskName"
/>
<button type="submit">Add Todo</button>
</form>
);
これを App
コンポーネントで読み込み、表示させます。
import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";
import { AddTodo } from "./AddTodo";
export const App = () => (
<React.Fragment>
<TodoListContainer />
<AddTodo />
</React.Fragment>
);
見た目の部分が追加できたの、入力したタスク名が取得できるようします。
useState
を使用して AddTodo.js
を以下のように変更しましょう。
import React, { useState } from "react";
export const AddTodo = () => {
const [taskName, setTaskName] = useState('');
const handleSubmit = e => {
e.preventDefault();
setTaskName('');
};
const handleChange = ({ target }) => {
setTaskName(target.value);
};
return (
<form
name="form"
onSubmit={handleSubmit}
>
<p>
{taskName}
</p>
<input
name="taskName"
value={taskName}
onChange={handleChange}
/>
<button type="submit">Add Todo</button>
</form>
);
};
useState
を使用して taskName
を設定し、input の onChange で値を取得できるようになりました。
また onSubmit で taskName
が空になるようにも設定しました。
taskName
の値を Store に追加できるようにします。
TodoList
で TodoListContainer
を作成したように、 AddTodo
のロジック部分を担う AddTodoContainer
を作成します。
AddTodoContainer.js
を作成し、以下のコードを記述します。
import { connect } from "react-redux";
import { AddTodo } from "../components/AddTodo";
export const AddTodoContainer = connect()(AddTodo);
AddTodoContainer.js
で行うことは「タスクを追加する」で、「タスクを追加する」は Action になります。
なので AddTodoContainer.js
が Store と紐づける必要があるのは Store の dispatch
です。
dispatch をどのような形でコンポーネントのプロパティと紐づけるかを mapDispatchToProps()
メソッドを使って定義します。
AddTodoContainer.js
内を以下のように変更します。
import { connect } from "react-redux";
import { AddTodo } from "../components/AddTodo";
import { addTodo } from "../actions"; // Action をインポートします
const mapDispatchToProps = { addTodo };
export const AddTodoContainer = connect(null, mapDispatchToProps)(AddTodo);
AddToContainer
でつなげた addTodo
を受け取って AddTodo
コンポーネントで dispatch できるようにします。
AddTodo.js
を以下のように変更します。
import React, { useState } from "react";
export const AddTodo = ({ addTodo }) => {
const [taskName, setTaskName] = useState('');
const handleSubmit = e => {
e.preventDefault();
addTodo(taskName);
setTaskName('');
};
const handleChange = ({ target }) => {
setTaskName(target.value);
};
return (
<form
name="form"
onSubmit={handleSubmit}
>
<input
name="taskName"
value={taskName}
onChange={handleChange}
/>
<button type="submit">Add Todo</button>
</form>
);
};
Store をつなげた AddTodoContainer.js
ができたので、 App
コンポーネントでの呼び出しを変更します。
import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";
import { AddTodoContainer } from "../containers/AddTodoContainer";
export const App = () => (
<React.Fragment>
<TodoListContainer />
<AddTodoContainer />
</React.Fragment>
);
これでタスクを追加することができるアプリができました。タスク名を入力し、Add Todo ボタンをクリックしてタスクが追加されるのが確認できます。
src/index.js
に確認のために追加していた dispatch()
メソッドを削除しておきます。
import React from 'react';
import { render } from 'react-dom';
import { store } from "./store";
// React Redux から `Provider` をインポート
import { Provider } from "react-redux";
import { App } from "./components/App";
render(
// <App /> を <Provider で囲む
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
また TodoListContainer.js
にある console.log も削除しておきます。
まとめ
Todo アプリの
- タスク一覧を表示する
- タスクを追加する
という機能を実装していきながら react-redux
モジュールの Provider
と connect()
を使って、
React コンポーネントと Redux の紐づけ方をまとめてみました。