#reduxとは?
UIのステートをアプリ全体で管理するためのフレームワーク。ReactやAngularJS、Vueなどで使用することができます。
##なぜreduxが必要なのか?
なぜreduxが必要なのかというと、ステートをアプリ全体で管理することで読みやすいコードを書けるからです。
reduxを使わなければ保守性が落ちてしまいます。
1.reduxを使わない
2.コンポーネント間のやり取りが増える
3.どこでステートが更新されたのかがわかりづらい
その問題を解決するのがreduxです。
1.ステートをアプリ全体で管理する
2.コンポーネント間のやり取りが減る
3.他のコンポーネントに依存しにくいコードが書ける
#reduxの流れ
reduxでのステートを更新の流れは主に4ステップです。
1.UIでイベントが発生
2.reduxに通知(イベントリスナー)
3.ステートの更新(イベントハンドラー)
4.UIのリレンダリング
###1.イベントの発生
ユーザーがボタンを押したり、文字を入力したタイミングでイベントが発生します。
###2.reduxへの通知
コンポーネントで発生したイベントに該当するアクションをストアーに渡し、
ステートの更新が必要なことを通知します。
アクションには、
・どんなイベントが発生したのか?(type属性)
・どんな値を渡すのか?(payload属性)
という情報が含まれます。
このアクションをストアーに通知するためにdispatchメソッドを経由します。
言い換えるとdispatchはイベントリスナーとしての働きです。
###3.ステートの更新
ストアーに変更が通知されるとstoreの中のreducerで
・引き渡されたアクション
・前回のステート
の2つを基に新しいステートに更新します。
言い換えるとreducerはイベントハンドラーとしての働きです。
###4.UIのリレンダリング
最後にストアーのステートが更新されるとUIに変更を通知します。
その後、UIはリレンダリングされます。
#reduxの実装方法
それでは、実際にReactでreduxを実装していきましょう。
サンプルプログラムは入力項目に入力した内容とセットした回数をカウントするプログラムです。
実装手順は次の5ステップ。
1.reducerの作成
2.アクションクリエーターの作成
3.ストアーの作成
4.ストアーとコンポーネントの連携
5.UIのイベントハンドラーの実装
##1.reducerの作成
まずはアクションがストアーに渡されたときに新しいステートに更新する処理を記述していきます。
// ステートの初期化
const initialState = {
count:0,
input:null
}
// リデューサーを定義
export default function reducer(state = initialState, action) {
switch(action.type) {
case 'SET_INPUT':
return {
count: state.count + 1 //カウントアップ
, input: action.input //入力内容をセット
}
default:
return state
}
}
reducer関数の引数は次の2つです。
・第一引数:前回のステート
・第二引数:アクション
アクションのtype属性で「どんな処理が発生したのか」がわかるので条件分岐させてイベントハンドラーを記述します。
今回のソースでは
・count:前回の回数に+1
・input:入力された値をセット
の2つのステートを更新します。
そして、デフォルト引数では「どんなステートがセットされるのか」を宣言します。
これにより、アクションが初めて呼ばれる際のステートを決めることができます。
##2.アクションクリエーターの作成
ストアーに引き渡すアクションを記述していきます。
export function setInput(input) {
return {
type: 'SET_INPUT',
input: input
}
}
アクションには
・type属性:どんな処理を行うのか?
・payload属性:どんな値が渡ってきたのか?
の2つを記述していきます。payload属性は任意ですが、type属性は必須です。
##3.ストアーの作成
アプリ全体で一つのストアーを管理したいためルートコンポーネントであるApp.jsで2つのことを行います。
・ストアーの作成
・ストアーの引き渡し
import React,{ Component } from 'react';
import { BrowserRouter, Switch, Route } from 'react-router-dom';
import { createStore } from 'redux'
import reducer from './redux/reducer'
import { Provider } from 'react-redux';
//screens
import click from './redux/click-con';
import show from './redux/show-con';
// 1.ストアの作成
const store = createStore(reducer);
class App extends Component {
render() {
return (
<BrowserRouter>
{/* 2.ストアーの引き渡し */}
<Provider store={store}>
<Switch>
<Route exact path="/click" component={click} />
<Route exact path="/show" component={show} />
</Switch>
</Provider>
</BrowserRouter>
);
}
}
export default App;
redux.createStoreメソッドに先ほど作ったreducerを渡して、ストアーを作成します。
このままだとストアーができただけで、コンポーネントにストアーが渡されていない状態です。
そのため、「react-redux」のProviderタグを使って各コンポーネントに渡します。
##4.ストアーとコンポーネントの連携
connectメソッドを使ってストアーとコンポーネントを連携します。
import { connect } from 'react-redux'
import { setInput } from './action'
import click from '../screen/click'
const mapStateToProps = state => {
const { input, count } = state
return { input, count }
}
const mapDispatchToProps = dispatch => {
return {
setInputClick: (input) => {
dispatch(setInput(input));
alert(`「${input}」がセットされました!`)
}
}
}
export default connect(mapStateToProps,mapDispatchToProps)(click)
import { connect } from 'react-redux'
import show from '../screen/show'
const mapStateToProps = state => {
const { input, count } = state
return { input, count }
}
export default connect(mapStateToProps)(show)
このconnectには主に2つの引数を渡します。
・mapStateToProps:必要なデータをストアーから取得
・mapDispatchToProps:ストアーにアクションを通知する関数
###mapStateToProps
ストアーから渡されたステートをコンポーネントに渡します。
・第一引数:現在のステート
・戻り値:コンポーネントに渡すステート
また、ストアーのステートをそのまま渡すだけではなく、mapStateToProps内でデータを整形して渡すことができます。
そして、戻り値に設定したステートはコンポーネントの引数に追加されます。
###mapDispatchToProps
コンポーネントにストアーに変更を通知するアクションクリエーターを渡します。
・第一引数:dispatchメソッド
・戻り値:ストアーに通知するアクションオブジェクト
dispatchメソッドにアクションオブジェクトを渡すことでストアーに通知できます。
mapStateToPropsと同じように戻り値に渡したオブジェクトはコンポーネントの引数に追加されます。
##5.UIのイベントハンドラーの実装
最後にUIのイベントハンドラーにconnectメソッドで引き渡されたdispatchでラップされたアクションクリエータを呼び出します。
import React, { useState } from 'react';
import { Link } from 'react-router-dom'
export default (props) => {
const style = {
textAlign: "center",
marginTop: "10px"
};
const [input, setInput] = useState(props.input);
/**
* inputを変更
*/
const changeInput = (e) => {
setInput(e.target.value);
}
return (
<div style={style}>
<input type="text" value={input} onChange={changeInput}></input>
<button onClick={() => props.setInputClick(input)}>値セット</button>
<br/><Link to="/show">表示へ</Link>
</div>
);
}
import React from 'react';
import { Link } from 'react-router-dom'
export default (props) => {
const divStyle = {
textAlign: "center",
marginTop: "10px",
"ul" : {
listStyle: "none"
}
};
const ulStyle = {
listStyle: "none"
}
return (
<div style={divStyle}>
<ul style={ulStyle}>
<li>カウント:{props.count}</li>
<li>値:{props.input}</li>
</ul>
<Link to="/click">セットへ</Link>
</div>
);
}
#QA
##Q.全てのステートを渡すのはダメ?
A.全てステートをコンポーネントに渡すことは可能ですが、避けたほうが無難です。
コンポーネントに渡すステートが変更されたかどうかを判定してUIはリレンダリングするのかを判断しています。
そのため、全てのステートを渡すと無駄にリレンダリングされる可能性があります。
パフォーマンスを良くしたいのであれば、必要なステートだけをコンポーネントに渡すのが最善です。
ちなみに再処理される条件は以下の通りです。
・mapStateToProps ⇒ ステートが変更されたとき
・UIのリレンダリング ⇒ mapStateToPropsの戻り値が変更されたとき
また、redux内部では変更判定をシャロー比較(===)しているため、配列処理等(Array.filter、 Array.concat)でデータを再生成している際は注意が必要です。