なるべくPureなReactと対比して書いているので、分かりやすくなってる(といいな。。)
なおハンズオンではありませんので悪しからず。
ツッコミどころはたくさんあるかもしれませんが、絞った内容とパターンを抑えて説明していきます。
まず何ができるの?
Reduxを導入することで何ができるのか、、、
まぁお馴染みですが、Propsのバケツリレーがなくなりますね。
実際に自プロジェクトでのReduxを導入したことによって得られたメリットをあげると、次のGifの様になります。
導入前
タブを切り替える際に毎回データのフェッチ、ローディングが走る
導入後
データをStoreで管理し無駄なデータのフェッチがなくなりローディングが毎回走らなくなった
Gifだとわかりにくいですが、導入前は下部のTabを切り替えるときにAPIへのfetch処理が毎回走ってしまうことでローディングが走っておりUXが悪くなってしまいます。
親のコンポーネントでデータを保持すればもちろんいけますが、複雑性が増したり、何よりナンセンスなのでReduxを導入しました。
これによってよりネイティブアプリに近い挙動を再現できています。
Reduxの概要
まずReduxに入ると、Fluxが〜とか単一方向に〜とか聞きますが、その辺りは今回は説明しません。
自分は物覚えも悪くそこから入っても最初「?」しか出なかった為です。
下記のReactプロジェクトでのコードを見ながら役割を比較して説明していきます。
なおReduxの細かいコード内容についてはまた今度描きます。
class Example extends React.Component {
constructor() {
super();
this.state = {
data: [],
}
this.fetchData = this.fetchData.bind(this);
}
fetchData() {
axios.get('/fetchData').then((res) => {
this.setState({
data: res.data.data,
});
})
}
render() {
return (
<button
onClick={this.fetchData}
>
Fetch Data
</button>
);
}
}
Store
StoreはStateを一括して保存してくれている貯め場の様な場所だと思ってください。
これは1つのプロジェクトにつき1つしか作りません。
上記のコード内には対応する部分はありません。
Reducer
Reducerは初期Stateの宣言、および後述するActionから渡ってきた指令によってSwitch分で分岐しながらStateを変更してくれる存在です。
ReactプロジェクトだとsetState()
をする感覚と同じです。
Action
Actionはその名の通り関数を定義するファイルになります。
Reactでの違いと比べるとこんな感じで、データを取得してきたり、Stateを変更するためのDataの準備をする行動 = Action
といった感覚になります。
公式には「Actionを発行する」とか言いますが、いきなり言われても意味わかりませんよね。。。
Reactにconnect
ここまで出てきているStore
、Reducer
、action
はReduxが持っているものです。
Reactにとっては全く無関係な状態です。
なのでReactとReduxを繋ぐ為にreact-redux
というライブラリを使います。
下記の実装で見ていきます。
実装 & 雛形
少し説明する箇所が多くなりますが、ざっくりと説明していきます。
想定としては、よくあるページのロード → APIからデータの取得 → データおよびビューの更新
です。
1. storeの定義
( 1 ) - createStore()
でReduxのstoreを使うよ!
的な宣言をします。中のcombineReducers
はreducerをまとめたオブジェクトをcombineReducers
を使うことによって2つ以上のReducerをまとめてくれています。
applyMiddleware(thunk)
の部分は、まずapplyMiddleware
はRedux用のサードパーティー製のライブラリを使う際、この中で定義してあげます。有名どころだとredux-devtoolsなど入れることが多いです。
そしてその中で定義しているthunk
ですが、これは本来Reduxでは非同期処理に対応していない為、非同期処理を可能にしてくれるライブラリになります。
非同期処理用のパッケージには
- 今回使っているredux-thunk(https://github.com/reduxjs/redux-thunk)
- redux-saga(https://github.com/redux-saga/redux-saga/blob/master/README_ja.md)
- redux-observable(https://github.com/redux-observable/redux-observable)
などがあり、各々メリット、デメリットがあります。
詳細は割愛しますが、今回は一番使いやすいredux-thunk
を使用します。
( 2 ) - 上記で作成したstore
をProvider
内で呼び出します。
import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import { Provider } from 'react-redux';
import { createStore, combineReducers, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import App from './User/pages/App';
import reducers from './reducers/';
const store = createStore(combineReducers(reducers), applyMiddleware(thunk)); // (1)
ReactDOM.render(
<Provider store={store}> // (2)
<BrowserRouter>
<Route path="/" component={App} />
</BrowserRouter>
</Provider>,
document.getElementById('root'),
);
2. reducerでinitialStateの定義
初期ステートを定義します。
上記で説明したReactでいうthis.state={}
の部分のようなものだと思っていてください。
最初はdefault argumentを与え、随時初期ステートを上書いて更新していく形になります。
const initState = {
data: [],
};
export default (state = initState, action) => {
const { type, payload } = action;
switch (type) {
default:
return state;
}
}
なお先ほどIndexでcombineReducers
の話がありましたが、2つ以上のReducerを定義する際には以下のようにオブジェクト型にラップしてエクスポートします。
import post from './post';
import sample from './sample';
export default {
post,
sample,
};
3. Componentでの呼び出し
ここがReduxとReactの繋がる大きなポイントになります。
( 1 ) - まずReactの際はただコンポーネントをエクスポートするだけでしたが、ReactとReduxを繋げる際はconnect
関数を使います。慣れてない方だと見慣れない構文ですがconnect()(<Component />)
とし括弧の2つ目にコンポーネントを記載します。
これはHOCs(ハイオーダーコンポーネント)、高階関数といいます。今回は割愛します。
(2) - そして括弧の1つ目のmapStateToProps
ですが、ここではStoreにある値を全て参照できます。Reducerで定義して、Storeに保存されている値を全て参照できるので、この中から必要なReducerの値だけを取ってきます。
- mapDispatchToPropsを定義するやり方もありますが、今回は定義しません。
(3) - connectしてきた値はPropsで受け取ることができます。
import React from 'react';
import { connect } from 'react-redux';
class Sample extends React.Component {
render() {
return (
<div>
{this.props.data.map(data => data.name)} // (3)
</div>
);
}
}
const mapStateToProps = ({ post }) => post; // (2)
export default connect(mapStateToProps)(Sample) // (1)
4. actionの定義
概念編で見た通り関数を定義します。
その際dispatch
を受け取ってdispatch
内でtype
、payload
定義します。
type
にはReducerで分岐させるためのTypeを渡します。
payload
には実際のデータを入れます。(実際にはpayloadと言う名前でなくても良いが、payloadが推奨されている)
import axios from 'axios';
export const FETCH_DATA = 'FETCH_DATA'; // (1)
export const fetchData = () => (dispatch) => {
axios.get('/fetchData').then((res) => {
dispatch({
type: FETCH_DATA,
payload: res.data.data,
});
});
};
5. reducerにてstateの更新
先ほどactionにてエクスポートしたtypeをインポートし、caseで分岐します。
その際にstateをpayloadのデータで上書きます。
+ import { FETCH_DATA } from '../actions/post';
const initState = {
data: [],
};
export default (state = initState, action) => {
const { type, payload } = action;
switch (type) {
+ case FETCH_DATA:
+ return {
+ ...state,
+ data: payload
+ };
default:
return state;
}
}
6. Componentにてactionsの呼び出し
あとは、actionで定義した関数を呼んであげるだけです。
今回のやり方だと、connect
するとdispatch
のPropsが渡ってきますので、dispatch
でラップした上で関数を呼んであげましょう。
import React from 'react';
import { connect } from 'react-redux';
+ import { fetchData } from '../actions/post';
class Sample extends React.Component {
+ componentDidMount() {
+ this.props.dispatch(fetchData());
+ }
render() {
return (
<div>
{this.props.data.map(data => data.name)}
</div>
);
}
}
const mapStateToProps = ({ post }) => post;
export default connect(mapStateToProps)(Sample)
まとめ
今回の内容は本に載ってる内容だったりとは少し違う書き方ですが、個人的には一番書きやすいと思った書き方です。
Reduxは複雑すぎると言う声が多いですが、自分なりの雛形を覚えてそこから発展させていけばそんなに難しく感じずに進められるのかなと思いました。(自分はかなり苦しんだ。。。。)
PureなReactがContext APIだったり、今回は関係ないけどHooksやSuspenceだったり充実してきているので、なん年後かには使われなくなっているんですかね。。。
まぁ、それにしてもReduxは書いてて楽しいです