ReactのRedux非同期処理がサルでも分かる超解説
移転しました。
この記事はかつての私と同じように「Reduxを使った非同期処理がいまいち分かんねー」という方に向けて書いた。とりあえずはReactの公式サイト、Reduxの公式サイト、Dan氏のReduxビデオ解説を観たが、なんかスッキリしない。特にReduxの非同期処理が分からない、という方向けの超シンプル解説。
Reactは公式サイトのチュートリアルなんかも充実していて丁寧だし分かりやすかった。しかしReduxは違う。特に公式サイトの非同期処理の例が変にややこしい。
こういうことをブログで書くと「アタシは公式サイトの説明を読んでも分からないバカです」と言ってるみたいだから、恥ずかしいしあまり書かれない。ウザいぐらいに「Reduxは素晴らしい。シンプル。カンタン」という発言がネット上にあふれている。
しかし私の頭ではパっと分からなかった。私以外でも「これ難しいなー」と思ってる人が居るんじゃないだろうか。仮に今は分かっていてもそこに達するまでにまーまー苦労したとか。オープンイノベーションの世界では「オレは習得するのに苦労したから、後続の人も同じ苦労をしろ」を根絶するべき。
したがって恥を忍んででも「ReactのRedux非同期処理がサルでも分かる超解説」を書くことにした。
この解説方法を一言で言うとこうなる。
まず先にReduxを使わないで非同期処理のコードをReactで書いて、その後でReduxを加える
これをやることでやっと理解できた。
本記事で最終的にできあがるコードの動くサンプル。
https://simple-example-redux.herokuapp.com/
これは単にサーバーに保存されているコメント群をとってきて表示するだけ。
ソースコード
クローンしてそのまま npm install してnpm startとすれば動きます。まだまだ学習中の身でもあるので「こうした方がいい」とかあったらぜひコメントください。
Reactだけの例
まずはReact だけを使った例
これはReactの基礎知識があれば把握できるレベルの単純なコード。
import React, { Component } from 'react' import ReactDOM from 'react-dom' class CommentList extends Component { constructor() { super(); this.state = { comments: [ { id: 1, comment: 'comment 1' }, { id: 2, comment: 'comment 2' }, { id: 3, comment: 'comment 3' }, { id: 4, comment: 'comment 4' } ], hasError: false, isLoading: false } } render() { if (this.state.hasError) { return <p>error</p>; } if (this.state.isLoading) { return <p>loading . . . </p>; } return ( <ul> {this.state.comments.map((item) => ( <li key={item.id}> {item.comment} </li> ))} </ul> ) } } ReactDOM.render( <CommentList />, document.getElementById('app') )
実行した結果
ソースコードをクローンした場合はコミットログのaacf3a3 "non-redux example"にして、npm startすれば以下の画面が出る。
constructorを見れば分かるようにstateには配列でcommentとboolean形式で2種類のステータスを入れている。
constructor() { super(); this.state = { comments: [ { id: 1, comment: 'comment 1' }, { id: 2, // : 省略 ], hasError: false, isLoading: false }
isLoading かもしくは hasErrorをtrueにすると、それぞれの表示に切り替わる。
APIからデータを取ってくる
ソースコードにcommentsを書き入れるのでは内容が変化しないので、そこをAPIからJSON形式で取ってくるように変更する。
import React, { Component } from 'react' import ReactDOM from 'react-dom' class CommentList extends Component { constructor() { super(); this.state = { comments: [] } } fetchData(url) { this.setState({ isLoading: true }); fetch(url) .then((response) => { if (!response.ok) { throw Error(response.statusText); } this.setState({ isLoading: false }); return response; }) .then((response) => response.json()) .then((comments) => this.setState({ comments })) .catch(() => this.setState({ hasErrored: true })); } componentDidMount() { this.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments'); } render() { if (this.state.hasError) { return <p>error</p>; } if (this.state.isLoading) { return <p>loading . . . </p>; } return ( <ul> {this.state.comments.map((item) => ( <li key={item.id}> {item.comment} </li> ))} </ul> ) } } ReactDOM.render( <CommentList />, document.getElementById('app') )
つまり以下のコードがmount時に実行されてコメントをAPIから取ってくる。
componentDidMount() { this.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments'); }
https://594ecc215fbb1a00117871a4.mockapi.io/commentsというのは無料で登録したモックで、アクセスすると5つのコメントをJSON形式で返してくる。本来ならここはRailsとかのサーバーにしてお好きなJSONを返すようにする。
動かした結果の画面はほぼ同じだが、コメントの中身はAPIから取ってきてますよ、と。
Reduxを入れる
では上記のコードにReduxを入れていく。まずはredux react-redux redux-thunkが必要になるのでそれらをインストールする。
npm install redux react-redux redux-thunk --save
念のためReduxの3原則
Three Principles · Redux
- Single source of truth(状態管理は1箇所だけ)
- State is read-only(状態は読み取り専用)
- Changes are made with pure functions(変更は純粋な関数で行う)
Reduxを入れてコードが完成した後のファイル構成
├── package.json └── src ├── actions │ └── comments.js ├── components │ └── CommentList.js ├── index.html ├── index.js ├── reducers │ ├── comments.js │ └── index.js └── store └── configureStore.js
Stateの内容
Redux無しのコードで明らかになったようにStateには3つのプロパティが必要。comments、hasError、isLoadingでありそれぞれにReduxアクションが必要になる。
src/actions/comments.js
export const getCommentsError = status => ({ type: 'GET_COMMENTS_ERROR', hasError: status }) export const loadComments = status => ({ type: 'LOAD_COMMENTS', isLoading: status }) export const fetchCommentsSuccess = comments => ({ type: 'FETCH_COMMENTS_SUCCESS', comments })
getCommentsErrorとloadCommentsはstatusを引数としてtype とステータスを返す。
fetchCommentsSuccessはデータの取り出しに成功したらコメントを配列に入れてcommentsとしてtype と共に返す。
アクションクリエータはアクションを返す。返すと書いているのにReturnが無い!となった方はこれは以下のように書いてるのと同じ。以下のコードをアロー関数で書いてreturnを省略しただけ。
export function getCommentsError(status){ return { type: 'GET_COMMENTS_ERROR', hasError: status }; }
アクションとしては元のRedux無しにあったfetchDataに相当するアクションがもうひとつ必要になる。ここではそれをfetchCommentsとして作成する。
src/actions/comments.js
export const fetchComments = url => { return (dispatch) => { dispatch(loadComments(true)); fetch(url) .then((response) => { if(!response.ok) { throw Error(response.statusText); } dispatch(loadComments(false)); return response; }) .then((response) => response.json()) .then((comments) => dispatch(fetchCommentsSuccess(comments))) .catch(() => dispatch(getCommentsError(true))); } }
reducers
reducersはstateとactionという2つの引数を持つ。reducersの中ではswitchを使ってaction.typeごとに処理を分けて、それぞれのactionを返す。
reducers/comments.js
export const getCommentsError = (state = false, action) => { switch (action.type) { case 'GET_COMMENTS_ERROR': return action.hasError; default: return state; } } export const loadComments = (state = false, action) => { switch (action.type) { case 'LOAD_COMMENTS': return action.isLoading; default: return state; } } export const comments = (state = [], action) => { switch (action.type) { case 'FETCH_COMMENTS_SUCCESS': return action.comments; default: return state; } }
それぞれのreducerをrootReducerでくっつける。
importでそれぞれのreducerをインポートする。後はcombineReducersで囲う。
reducer名をもっとシンプルにgetErrorとかloadとかでも良かったんじゃないの?と思うかもしれないが、ここはできるだけcommentsという主語を入れた名前の方がいい。
今回の例では全てのreducerはcommentsに関することだが、これ以降にusers、likes、とか色んなreducerを扱うようになった時に混乱しないため。
reducers/index.js
import { combineReducers } from 'redux'; import { getCommentsError, loadComments, comments } from './comments'; export default combineReducers({ getCommentsError, loadComments, comments, });
Store
ここはほぼ全てのReduxの解説にある内容と同じ。こうしてStore作りますよ、と。
store/configureStore.js
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from '../reducers'; const configureStore = initialState => { return createStore( rootReducer, initialState, applyMiddleware(thunk) ); } export default configureStore
index.js
import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import configureStore from './store/configureStore'; import CommentList from './components/CommentList'; const store = configureStore(); render( <Provider store={store}> <CommentList /> </Provider>, document.getElementById('app') );
Components
components/CommentList.js
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { fetchComments } from '../actions/comments'; class CommentList extends Component { componentDidMount() { this.props.fetchData('https://594ecc215fbb1a00117871a4.mockapi.io/comments'); } render() { if (this.props.hasError) { return <p> error </p>; } if (this.props.isLoading) { return <p> Loading...</p>; } return ( <ul> {this.props.comments.map((item) => ( <li key={item.id}> {item.comment} </li> ))} </ul> ); } } CommentList.propTypes = { fetchData: PropTypes.func.isRequired, comments: PropTypes.array.isRequired, hasError: PropTypes.bool.isRequired, isLoading: PropTypes.bool.isRequired }; const mapStateToProps = state => ({ comments: state.comments, hasError: state.getCommentsError, isLoading: state.loadComments }); const mapDispatchToProps= dispatch => ({ fetchData: (url) => dispatch(fetchComments(url)) }); export default connect(mapStateToProps, mapDispatchToProps)(CommentList);
まず importしているconnectがcomponentをstore につなげる役割をする。
actionsからはfetchCommentsのみをimport する。ここで必要なのはこのアクションだけで他のはdispachして呼び出す。
後はもう細かい説明よりコード見た方がいい。
JavaScript界隈に足を踏み入れる前に以下の本を読んで、結構参考になった。バリバリのフロントエンドのエンジニアには不要だが、これからフロントエンドも知っておこうかな、ぐらいの人にはちょうどいい内容だった。
- 作者: 松田承一
- 発売日: 2016/08/25
- メディア: Kindle版
- この商品を含むブログを見る