- Published on
ReactのRedux非同期処理がサルでも分かる超解説
- Authors
- ジャバ・ザ・ハットリ
この記事はかつての私と同じように「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 界隈に足を踏み入れる前に以下の本を読んで、結構参考になった。バリバリのフロントエンドのエンジニアには不要だが、これからフロントエンドも知っておこうかな、ぐらいの人にはちょうどいい内容だった。
いまから始める Web フロントエンド開発 |
作者: 松田承一 |
発売日: 2016/08/25 |
メディア: Kindle 版 |