Reduxとは
facebook社が提案したflux
を元に、javascriptでアプリのステート管理を行うためのツール。
各コンポーネントで持っていたstate
や関数をstore
やreducer
と呼ばれる場所で持つことで、全てのコンポーネントからアクセス可能となる。
必要性
大規模なアプリケーションになり、stateが多く存在するときなどに導入するといいらしいです。
stateが多くなればなるほど、バケツリレーが大量に発生し、エラーが発生する可能性が大きくなってしまう為です。
このへんは検索すればたくさん出てくるので、redux
のフローなども含めて調べてみてください。
本記事の目的
僕自身アルバイトでこれからReactを書ける環境に行けることや、製作中の個人ブログのフロント部分をReactで書くために学びたいと思っていたのでメモしたいと思い、書きました。
初めは個人ブログのフロントをflux
も使わずに書いていたのですが、同じようなstateが複数出てきたときに管理が難しくなったため、flux
かredux
を用いることに決めました。
redux
に決めた要因としては、facebook社が提供するflux
が2年前くらいから更新が止まっているとの情報から、学習コストは多少高くてもredux
を覚えてしまおうというところです。
では、見ていきましょう。
導入
完成図

環境
環境はこのような感じです。
macOS mojave
node v12.6.0
redux ^4.0.1
yarn 1.17.0
本記事ではyarn
を用います。npm
の方は適宜読み替えてください。
yarn
はbrew
でインストールすることが推奨されているようです。
create-react-app
まずnpx
を用いてプロジェクトを作成します。
npx create-react-app todo-tutorial-redux
次に、一旦作成されたプロジェクトの起動確認をしていきます。
cd todo-tutorial-redux
yarn start
ロゴが表示されたらokです。
次に、、必要なツールを入れていきます。
yarn add redux react-redux
これで準備は完了です。
Container ComponentsとPresentational Components
いきなりですが、公式tutorialの順番とは大きく入れ替わって、まずはContainer Components
とPresentational Components
について見ていきます。
redux
ではコンポーネントの種類を大きくこの2つに分けており、それぞれのルールに則って作ることで、再利用性の高いコードが書けるようになります。
こちらを参考にさせていただきました
Presentational Component
このコンポーネントは見た目を担当するコンポーネントで、reactで普段用いるコンポーネントと変わりありません。
- stateを基本持たない(持つ場合はデータではなく、UIの状態)
-
Actions
,Store
(reduxの他の要素)に依存しない - 基本的に
functional components
として書かれる
Container Components
redux
のconnect
という機能を用いることで、ロジックに関与することができるコンポーネントになります。
通常、このコンポーネントはスタイルを持たず、下位のコンポーネントのデータソースとして機能します。
mapStateToProps
とmapDispatchToProps
というところがキモで、
-
mapStateToProps
:store
で管理するstate
をprops
として扱うことができる。 -
mapDispatchToProps
:Actions
で定義した関数をprops
として扱うことができる。
これを使って、redux
管理をしているものをPresentationl Components
で表示したり、関数を実行したりできます。
ディレクトリ構造
公式tutorialでは、Presentational Components
をComponents
ディレクトリに、Container Components
をContainers
ディレクトリに分けています。
しかし、これらを分けることは必須ではなく、まとめて記述しても問題ありません。
Actions
ここからはtutorial通りに進めていきます。Actions
はstore
に対しての動作を書くところです。
何かが起きたときに、起きたことによって何をするか、そのときに用いるデータと共に定義しておき、Reducers
へ報告します。(Actions
の時点ではstore
の状態は変化しません。)
そして、これを実際に呼び出すところがContainer Components
です。
Actions
は、action types
というユーザが行った動作を文字列で指定したものを持ちます。この文字列は次に説明するReducers
と対応します。
役割としては、Actions
で発火、Reducers
で処理をする際の命令を投げる先の指定といったところです。
export const ADD_TODO = 'ADD_TODO'
そして、action creators
でaction types
で定義したデータとそのアクションに必要なデータをまとめます。Reducers
はここから呼び出されます。
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
単純な関数として記述されていることがわかります。ユーザがTODOを追加したとき、そのaction types
とその動作に必要なデータとしてtext
を返しています。
actions.js
/*
* action types
*/
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'
/*
* other constants
*/
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
/*
* action creators
*/
export function addTodo(text) {
return { type: ADD_TODO, text }
}
export function toggleTodo(index) {
return { type: TOGGLE_TODO, index }
}
export function setVisibilityFilter(filter) {
return { type: SET_VISIBILITY_FILTER, filter }
}
Reducers
Reducers
はアクションから何が起きたかをしらされたときに実際に行なう処理を書きます。
その処理は、以下のように
(previousState, action) => newState
過去のstate
と処理の内容から新しいものを生み出すというものです。
Reducers
の注意点として、
- 引数が不自然に変更されてはいけない(同じ引数を渡したら常に同じ結果を返す)
- 非同期処理など副作用を生み出すものを書けない(
redux-thunk
やredux-saga
といったMiddleWare
を使わないといけない) -
Date
や乱数などのnon-pure function
は使用できない
という点があります。
Reducersの返す値
Reducers
はアクションを受け取ってなにか処理を行い、反映させます。
しかし、初回の表示の際に表示するデータが定義されていない場合、うまく動かないことがあるため、アクションがない場合にはinitialState
を定義しておいてそれを返すようにします。
function todoApp(state = initialState, action) {
// For now, don't handle any actions
// and just return the state given to us.
return state
}
重要なのはstate = initialState
とreturn state
です。
実際に処理のあるパターンを見てみましょう。
import {
ADD_TODO
} from './actions'
...
function todoApp(state = initialState, action) {
switch (action.type) {
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
default:
return state
}
}
action types
をインポートして、それを元にswitch
で処理を選択しています。
先程書いた初期データを返す命令はdefault
で定義されます。
- この
reducers
でADD_TODO
のアクションが呼ばれたら -
state
を今のstate
(...state.todos
)と送られてきたtext
を追加したものをとして更新して返す
といった処理をしています。
Reducersの分割
処理を書いていくうちに、規模が大きくなったり機能が入り混じったりして、見通しが悪くなってしまうことが出てくると思います。
以下の例では、TODO一覧をフィルター表示するためのものとTODOを追加・トグルするものというもので分けたほうが見やすそうです。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
redux
はreducer
を1つしか返すことができません。しかし、複数のreducer
をまとめ上げたものを返すことは可能です。そのための機能として、combineReducers
というものがあります。
以下のように、todos
とvisibilityFilter
というreducer
に分ける場合、
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
このようにまとめることができます。
import { combineReducers } from 'redux'
const todoApp = combineReducers({
visibilityFilter,
todos
})
reducers.js
import { combineReducers } from 'redux'
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters
} from './actions'
const { SHOW_ALL } = VisibilityFilters
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp
Store
Store
はアプリケーションにつき1つしか存在しません。
役割として、
-
getState()
でstate
へアクセスできる -
dispatch(action)
でstate
を更新できる -
subscribe(listener)
でリスナーを登録できる -
subscribe
で登録した関数でunsubscribe
できる
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// Log the initial state
console.log(store.getState())
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() => console.log(store.getState()))
// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
// Stop listening to state updates
unsubscribe()


store.js
combineでReducers
でまとめたreducers
を元にstore
を作成します。
import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
Component
以下の設計でTODOListを作成します。
Presentational Components
- TodoList : TODOのリスト表示
- todos :
{id, text, completed}
という構成の配列 - onTodoClick(id: number) : 未完了・完了をtoggleする
- todos :
- Todo : TODO単体
- text: string : 本文
- completed: boolean : TODOの状態
- onClick() : callback
- Link : callback
- onClick()
- Footer : 表示フィルターの選択
- App : root component
Container Components
- VisibleTodoList : TODOのフィルター後のデータを保持する
- FilterLink : 現在
Link
で選択されているフィルターからフィルターを行う- filter: string
Other Components
- AddTodo : Addボタン
components/Todo.js
import React from 'react'
import PropTypes from 'prop-types'
const Todo = ({ onClick, completed, text }) => (
<li
onClick={onClick}
style={{
textDecoration: completed ? 'line-through' : 'none'
}}
>
{text}
</li>
)
Todo.propTypes = {
onClick: PropTypes.func.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}
export default Todo
単純にTodoを表示し、onTodoClick(index)
を実行を渡します。
components/TodoList.js
import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, onTodoClick }) => (
<ul>
{todos.map((todo, index) => (
<Todo key={index} {...todo} onClick={() => onTodoClick(index)} />
))}
</ul>
)
TodoList.propTypes = {
todos: PropTypes.arrayOf(
PropTypes.shape({
id: PropTypes.number.isRequired,
completed: PropTypes.bool.isRequired,
text: PropTypes.string.isRequired
}).isRequired
).isRequired,
onTodoClick: PropTypes.func.isRequired
}
export default TodoList
todos
を全て表示します。
container/VisibleTodoList.js
import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
TodoList
で表示に用いているtodos
をフィルターしています。
filter
の値によって、getVisibleTodos
でtodos
の内容が変わっていることがわかると思います。
mapStateToProps
はgetVisibleTodos
に現在のtodos
とフィルターの種類を渡すことで、新しいtodos
を定義しています。
mapDispatchToProps
はTodo
コンポーネントがクリックされたときに発火し、toggleTodo
をdispatch
します。
また、connect
を用いて、TodoList
コンポーネントで定義した関数を使えるようにしています。
components/Footer.js
import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'
const Footer = () => (
<p>
Show: <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
{', '}
<FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
{', '}
<FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
</p>
)
export default Footer
フィルターのaction types
をFilterLink
へ渡しています。
components/Link.js
import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => {
if (active) {
return <span>{children}</span>
}
return (
<a
href=""
onClick={e => {
e.preventDefault()
onClick()
}}
>
{children}
</a>
)
}
Link.propTypes = {
active: PropTypes.bool.isRequired,
children: PropTypes.node.isRequired,
onClick: PropTypes.func.isRequired
}
export default Link
選択されている状態の場合はただのspan
で、選択されていない場合はonClick
を実行するa
タグを出力する。
containers/FilterLink.js
import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
const mapDispatchToProps = (dispatch, ownProps) => {
return {
onClick: () => {
dispatch(setVisibilityFilter(ownProps.filter))
}
}
}
const FilterLink = connect(
mapStateToProps,
mapDispatchToProps
)(Link)
export default FilterLink
まず、ownProps.filter
はFooter
で選択されたフィルターを持ちます。
Link
がクリックされると、setVisibilityFilter
がdispatch
されます。
そして、押されたリンクはactive
を持ち、そのリンクだけがspan
で表示されるようになります。
参考:reducer部分
...
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
...
containers/AddTodo.js
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
let AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form
onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}
>
<input
ref={node => {
input = node
}}
/>
<button type="submit">Add Todo</button>
</form>
</div>
)
}
AddTodo = connect()(AddTodo)
export default AddTodo
このコンポーネントはContainer Components
ですが、スタイルも入っている例です。
AddTodo
はinput
の値で変わるため、let
で宣言されています。
送信されたら、入力内容をaddTodo
にdispatch
しています。
components/App.js
import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
<div>
<AddTodo />
<VisibleTodoList />
<Footer />
</div>
)
export default App
index.js
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
const store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
index
では、Provider
を使ってstore
を使えるようにする必要があります。
まとめ
読んでみると少し理解が深まった気がしますが、非同期通信を用いた方法や、複雑な処理を行う場合、自分で書くためにはもう少し勉強しなきゃなと感じました。
次は、非同期通信を用いたパターンについてまとめる予定です。
ご指摘、アドバイスがあれば宜しくおねがいします。