サイバー・バズの@yuinchirnです。
弊社で最近リリースした「Cloud-F」というアパレルのクラウド展示会サービスは、フロントサイドはReact + Redux + Webpack、サーバサイドはScala + Akka HTTPで作られています。
今回は、フロント側のReact + Reduxを使ったプロジェクトの構成について紹介していきます。
React + Reduxプロジェクトを作る上で参考になればと思っております。
下記記事を読んで基本を押さえておくと読みやすいと思います。
全体のディレクトリ構成
今回はバイヤー向け管理画面、ブランド向け管理画面という二つの画面を作るため、
両方のプロジェクトで共通で利用する部分は/common
バイヤー向け管理画面は/buyer
ブランド向け管理画面は/brand
という3つのReact + Reduxプロジェクトからなる構成にしました。
├── common(共通プロジェクト)
│ ├── src
│ │ ├── assets
│ │ │ └── sass(scss置き場)
│ │ ├── containers(reduxのstateが関わるcomponent)
│ │ ├── components(reactの純粋なcomponent)
│ │ ├── utils(共通Utilメソッド系)
│ │ └── redux(Redux関連のディレクトリ)
│ │ └── modules(Action,ActionType,Reducerをまとめたファイルの置き場)
│ └── package.json
├── buyer(子プロジェクト)
│ ├── public
│ │ ├── assets
│ │ │ └── js
│ │ │ └── bundle.js
│ │ └── index.html
│ ├── src
│ │ ├── assets
│ │ │ └── sass(scss置き場)
│ │ ├── containers(reduxのstateが関わるcomponent)
│ │ ├── redux(Redux関連のディレクトリ)
│ │ │ ├── modules(Action,ActionType,Reducerをまとめたファイルの置き場)
│ │ │ ├── ConfigureStores.js(Storeの設定を記載したファイル)
│ │ │ └── RootReducer.js(Reducerをまとめたファイル)
│ │ ├── routes.js(ルーティングを定義しているファイル)
│ │ └── index.js(大元のjsファイル)
│ ├── package.json
│ └── webpack.config.js
├── brand(子プロジェクト)
│ ├── public
│ │ ├── assets
│ │ │ └── js
│ │ │ └── bundle.js
│ │ └── index.html
│ ├── src
│ │ ├── assets
│ │ │ └── sass(scss置き場)
│ │ ├── containers(reduxのstateが関わるcomponent)
│ │ ├── redux(Redux関連のディレクトリ)
│ │ │ ├── modules(Action,ActionType,Reducerをまとめたファイルの置き場)
│ │ │ ├── ConfigureStores.js(Storeの設定を記載したファイル)
│ │ │ └── RootReducer.js(Reducerをまとめたファイル)
│ │ ├── routes.js(ルーティングを定義しているファイル)
│ │ └── index.js(大元のjsファイル)
│ ├── package.json
│ └── webpack.config.js
各ディレクトリの詳細
上記で紹介したディレクトリ一つ一つの役割を細かく紹介していきます。
/components
- reduxのstateを持たない純粋なreactのcomponentを置くディレクトリです。
これらは基本的に、各ページを構成するパーツなので複数のファイルから参照されます。
そのため、commonプロジェクト配下に配置してどの子プロジェクトからも参照できるようにします。
ex.
import React from'react'
import {PropTypes} from 'react'
export default class ListMenu extends React.Component {
static propTypes = {
title: PropTypes.string,
items: PropTypes.array
}
constructor(props) {
super(props);
}
componentWillReceiveProps(nextProps) {}
render() {
const {items,title} = this.props
return (
<div>
<h2>{title}</h2>
<ul>
{items && items.map((item, e) => {
return (
<li className="c-menu__item">
<Link to={item.href}>{item.name}</Link>
</li>
)
})}
</ul>
</div>
)
}
}
/containers
- Reduxのstateを持ったreactのcomponentを置くディレクトリです。
上記で説明したComponentsとの違いはReduxのstateを持つか持たないかです。
下記ではログイン画面のcontainerを例として紹介しています。
基本的に、似たようなページを一つのグループとしてディレクトリを作り(auth,profileなど)、
その配下に各ページ単位で一つcontainerのjsファイルを作成しています。
また、サイドメニュー、ヘッダー、フッターなど各ページグループで共通に利用する部分は各ページグループディレクトリ配下のpartsディレクトリに切り出しています。
ディレクトリ構成
├── containers
├── auth
│ ├── parts
│ │ └── sideBar.js
│ ├── signin.js
│ ├── signup.js
│ ├── passwordRemind.js
│ ├── verify.js
└── profile
├── parts
│ └── sideBarIndex.js
│ └── sideBarList.js
├── index.js
└── list.js
配置するファイル例
import { bindActionCreators } from 'redux'
import React from 'react';
import ReactDOM from 'react-dom'
import {PropTypes} from 'react'
import { Link } from 'react-router'
import { reduxForm, Field } from 'redux-form'
import { connect } from 'react-redux'
import { replace } from 'react-router-redux'
import { routeActions } from 'react-router-redux'
import { signIn } from '../../redux/modules/SignIn'
import Button from 'common/components/particle/button/Index'
import FormField from 'common/components/form/field/Index'
import FieldInput from 'common/components/form/field/FieldInput'
class SignIn extends React.Component {
static propTypes = {
handleSubmit : PropTypes.func.isRequired,
replace : PropTypes.func.isRequired,
}
constructor(props) {
super(props)
}
componentWillMount() {}
componentWillReceiveProps(nextProps) {}
handleSubmit(values) {
this.props.signIn(values.email, values.password)
}
render() {
const {handleSubmit, status, errors} = this.props;
return (
<div>
<div className="c-card__content">
<form className="c-form" onSubmit={handleSubmit(this.handleSubmit.bind(this))}>
<FormGroup title={'メールアドレス'}>
<Field type="email" component={FieldInput} name="email" errors={errors} placeholder="example@cloud-f.com" />
</FormGroup>
<FormGroup title={'パスワード'}>
<Field type="password" component={FieldInput} errors={errors} name="password" />
</FormGroup>
<FormGroup>
<Button type="submit">ログイン</Button>
</FormGroup>
</form>
</div>
<div>
<p><Link to="/verify">登録がお済みでない方はこちら </Link></p>
</div>
<div>
<p><Link to="/password/reset">パスワードを忘れた方はこちら </Link></p>
</div>
</div>
)
}
}
function mapStateToProps(state) {
return {
errors: state.signIn.fetch.errors,
status : state.signIn.fetch.status,
auth : state.auth,
succeeded: state.signIn.fetch.succeeded
}
}
function mapDispatchToProps(dispatch) {
return bindActionCreators({
...{
replace,
signIn,
searchIcon,
clearFetch
},...routeActions}, dispatch)
}
export default connect(mapStateToProps, mapDispatchToProps)(SignIn)
/redux/modules
・ reduxの「Action, Reducer」をまとめたものを機能別で分けたファイルを置くディレクトリ
基本的には上で説明したcontainersと同じ単位でreducerを作成しています。
使われている箇所、扱っているものを把握しやすくなりオススメです。
ディレクトリ構成
├── redux
├── modules
│ ├── Auth.js
│ ├── Profile.js
│ ├── Product.js
│ └── ...
└── RootReducer.js
配置するファイル例
import {api as API} from 'Config'
import {CALL_API} from 'common/redux/middlewares/Client'
// Action types
export const REQUEST_SIGN_IN = 'REQUEST_SIGN_IN'
export const SUCCESS_SIGN_IN = 'SUCCESS_SIGN_IN'
export const FAILED_SIGN_IN = 'FAILED_SIGN_IN'
// Action
export function signIn(email, password) {
return {
[CALL_API]: {
api: API.signIn,
requestTypes: [REQUEST_SIGN_IN],
successTypes: [SUCCESS_AUTH],
failedTypes: [FAILED_SIGN_IN],
body: {email: email, password: password}
}
}
}
// Reducer
export function signInReducer(state = {errors: {}}, action) {
const { type } = action
switch (type) {
case REQUEST_SIGN_IN:
case SUCCESS_SIGN_IN:
case FAILED_SIGN_IN:
default:
return state
}
}
ActionTypeの定義、Action、ReducerをContainersと同じ単位で一つのファイルにまとめることによって、下記のようなメリットがありました。
- Action → Reducerの一連の処理内容を把握しやすい
- Actionが実際に発行されているContainersとActionの関連性が分かりやすい
/middleware
・ Action → Reducerの間に共通で行う処理を担うmiddlewareを置くディレクトリ
そもそもmiddlewareとは?
MiddlewareとはAction → Reducerの前後に任意の処理を追加できるReduxの概念のことです。
今回のプロジェクトでは、APIの呼び出しの処理をmiddlewareで記述しています。
※redux-thunk
というライブラリを利用。
middlewareについて詳しく知りたい方は下記を読んでみてください。
「middlewareによって何が解決されるか?」が例を使って説明されているので分かりやすいです。
middlewareで記述した内容
- Actionで指定したapiを叩く。
- レスポンス(エラー、成功)によってActionを出し分ける
配置するファイル例
※流れを把握しやすくするためにコードは省略しています。
// Middleware
export const CALL_API = Symbol('Call_API')
// Actionをdispatchするたびに
export default store => next => action => {
const callAPI = action[CALL_API]
requestTypes.forEach(t => {
next(actionWith({type: t}))
})
function actionWith(data) {
const finalAction = {...action, ...data}
delete finalAction[CALL_API]
return finalAction
}
// Actionで指定したAPIを叩く
return fetch(callAPI.api, options)
.catch((e) =>{
// エラー
// エラー時のActionを発行
failedTypes.forEach(t => {
next(actionWith({
type: t,
status: error.status,
errors: errors
}))
})
})
.then(response => {
// 成功
// リクエスト成功時のActionを発行
successTypes.forEach(t => {
next(actionWith({
type: t,
response: response.body,
status : response.status
}))
})
})
}
上記のようにAPIの処理をmiddlewareに共通処理として切り出すことで、
APIを叩く際は下記のようなActionを発行するだけで済み、とてもシンプルにまとまります。
また、呼び出したAPIの種類やRequestBodyの出力をmiddlewareで行うことでデバッグも快適に行うことができました。
ReduxでのAPI呼び出しはmiddlewareで間違いなさそうです。
// Action
export function signIn(email, password) {
return {
[CALL_API]: {
api: "/signin",
requestTypes: [REQUEST_SIGN_IN],
successTypes: [SUCCESS_AUTH],
failedTypes: [FAILED_SIGN_IN],
body: {email: email, password: password}
}
}
}
まとめ
今回は、弊社のReact + Reduxのプロジェクトを構成について簡単にご紹介させていただきました。
後日、この構成を実現させるための設定等をご紹介できればと思っております。