あーあ、hooks出ちゃったからあまり今後使わない可能性あるんだけど、せっかく作ったし記事にしようかなと。
はじめに
create-react-app使ってReactプロダクトを始めるでも全然ありだと思うんですけど、結局必要なものを自分で入れなければいけない。
例えば、redux,redux-form,recompose,styled-components, prettier etc
とか。
結局入れるもの多いし、ディレクトリ構成なんかもちゃんとやろうとすると最初からだとめんどくさい。それに、Reactのプロダクトをいくつか作らさせてもらっていて、自分的なベストプラクティスも固まってきた。だったら、最初からできているもの作っちゃえば楽じゃないってことで作ってみた感じ。
ただ、後述しますがReact Hooksが今後出るからちょっとな感は否めない・・・
GitHub - Takumi0901/ts-react-redux-boilerplate: TypeScript + React + Redux Counter Example
自分の中でのレギュラー
Reactプロジェクト始めるうえで私的に必要最低限のものとして列挙。こう並べてみると結構あるんですよね。
ただ、FlowよりはTypeScript!新規プロジェクトならなおさらTypeScript!
typescript-fsaが無いaction、reducerはもう嫌だ!
Reduxでやる理由の一つにRedux DevTools Extension!
Prettierで自動整形してくれないと気持ち悪い体になってしまった!
React Hot Loader無いともう無理!
Contains
- typescript 3.1
- react 16.6
- redux 4.0
- react-router 4.3
- react-router-redux 4.0
- redux-form 7.4
- redux-thunk 2.3
- recompose 0.30.0
- styled-components 3.4.10
- styled-components-ts 0.0.14
- immutable 4.0.0-rc.10
- typescript-fsa 3.0.0-beta-2
- typescript-fsa-reducers 0.4.5
- material-ui 0.20.2
- Redux DevTools Extension
Build tools
- Webpack 4
- Webpack Dev Server
- Typescript Loader
- React Hot Loader
- HTML Webpack Plugin
- Uglifyjs Webpack Plugin
- Prettier
- Lint staged & Husky
ディレクトリ構成について
だいたいディレクトリ構成はこれで定着してきた感。詳細はあとで説明するとして個人的にはcontainersの中身は結構気に入っている。ViewとReduxのコネクト部分をうまく切り分けることができたかなと。Viewが切り分けられていることによって今後Hooksが正式リリースされても対応しやすいのではと安易に考えている部分もある。
.
├── public
│ └── assets
├── src
│ ├── api
│ ├── components
│ │ └── atoms
│ ├── containers
│ │ ├── app
│ │ │ ├── enhancers
│ │ │ └── organisms
│ │ ├── home
│ │ │ ├── enhancers
│ │ │ ├── organisms
│ │ │ └── pages
│ │ └── users
│ │ ├── enhancers
│ │ ├── organisms
│ │ └── pages
│ ├── redux
│ │ ├── middleware
│ │ └── modules
│ │ ├── counter
│ │ └── users
│ └── styles
├── webpack
├── webpack.common.js
├── webpack.config.js
├── webpack.production.config.js
// ・・・
apiディレクトリ
ここではAPIとのやり取りを行う。
特に変わりばえはしない感じだと思う。
componentsディレクトリ
ボイラープレートでは特に何も作っていない。
基本的に見た目の部分はmaterial-uiを使用している。ただ、プロダクトによってはmaterial-uiを使わずに自前でComponentを作る場合もあるはず。その場合はatomsやmoleculesをつくる。
あとは必ずステートレスに作ること。components配下のものはReduxとのコネクトはしてはいけないルール。
containersディレクトリ
ここではAtomic Designでゆうところのpagesごとにディレクトリをきっている。そのページごとに必要な機能を実装していくイメージ。
- enhancers: recomposeを使ってreduxをコネクト
- organisms: 機能ごとのView
- pages: そのページで使うorganismsをまとめたただのステートレスComponent
pages/Home.tsx
const Home: React.SFC<Props> = () => {
return (
<React.Fragment>
<Helmet>
<title>title</title>
</Helmet>
<CounterActions />
<FormFieldsElement />
</React.Fragment>
)
}
export default withRouter<any>(Home)
organisims/CounterActions.tsx
const CounterActions: React.SFC<Props> = ({ counter, increment, decrement }) => {
return (
<Paper className={'u-pt-24 u-pr-24 u-pb-24 u-pl-24 u-mb-24'} square elevation={0}>
<Typography variant="headline">Counter!!</Typography>
<Typography variant="display1">{counter.count}</Typography>
<Button variant="contained" color="primary" onClick={() => increment(1)}>
+
</Button>
<Button variant="contained" color="secondary" onClick={() => decrement(-1)}>
-
</Button>
</Paper>
)
}
export default counterEnhancer(CounterActions)
enhancers/Counter.ts
export interface Props {
increment(count: number): void
decrement(count: number): void
counter: ICounter
}
const connector = connect(
(state: IStore) => {
return {
counter: state.counter
}
},
dispatch => Redux.bindActionCreators({ increment, decrement }, dispatch)
)
export const counterEnhancer = compose<Props, {}>(
connector,
onlyUpdateForKeys(['counter']),
lifecycle({
componentDidMount() {
console.log('mounted')
}
})
)
redux
middleare
ここではstateに渡される前に共通の処理を行う
今回はスネークケースとキャメルケースの変換をしている
例えばリクエスト時はスネークケースからキャメルケースへの変換をする
こういった処理をリポジトリ層を作ってやっていた時もあったけど、変換するのをあまり意識せずにできるので気に入っている
import { decamelizeKeys } from 'humps'
export default _ => (next: any) => (action: any) => {
if (action.type.match(/post.*REQUEST$/)) {
action.payload = decamelizeKeys(action.payload)
}
next(action)
あとはstoreでかませればOK
const configureStore = (initialState?: IStore): Redux.Store<IStore> => {
let composes
if (process.env.NODE_ENV === 'development') {
const composeEnhancers =
typeof window === 'object' && window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']
? window['__REDUX_DEVTOOLS_EXTENSION_COMPOSE__']({})
: Redux.compose
composes = composeEnhancers(Redux.applyMiddleware(thunk, responseCamelizer, requestDecamelizer))
} else {
composes = Redux.compose(Redux.applyMiddleware(thunk, responseCamelizer, requestDecamelizer))
}
return Redux.createStore(rootReducer, initialState, composes)
}
export default configureStore
modules
ここはデータごとに切り分けていて、Re-ducksパターンっぽい?のを採用している。
alexnm/re-ducks: An attempt to extend the original proposal for redux modular architecture: https://github.com/erikras/ducks-modular-redux
modeles/counter/index.ts
typescript-fsaとtypescript-fsa-reducersを使う
これのおかげでかなりスッキリ書くことができる
import { CounterRecord } from 'src/redux/modules/counter/records'
import actionCreatorFactory from 'typescript-fsa'
import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'
// Action types
const actionCreator = actionCreatorFactory()
export enum ActionTypes {
INCREMENT = 'counter/INCREMENT',
DECREMENT = 'counter/DECREMENT'
}
export const increment = actionCreator<number>(ActionTypes.INCREMENT)
export const decrement = actionCreator<number>(ActionTypes.DECREMENT)
// Reducer
export const countUpReducer = reducerWithInitialState(new CounterRecord())
.case(increment, (state, payload) => state.inc(payload))
.case(decrement, (state, payload) => state.dec(payload))
modeles/counter/records.ts
immutable.jsを使う
reducer周りでごにょごにょするとかなり見栄え悪くなるのを防げる
reducerは交通整理のみを行うことができる
import { Record } from 'immutable'
import { ICounter } from 'src/redux/modules/counter/types'
export class CounterRecord extends Record({ count: 0 }) implements ICounter {
inc(payload: number) {
return this.withMutations(s => s.set('count', this.count + payload))
}
dec(payload: number) {
return this.withMutations(s => s.set('count', this.count + payload))
}
}
styles
スタイルに関する部分でstyled-components使ってbase.cssやreset.cssを提供している。
utilityStyles
ちょっとマージンつけたいとか、colorを変更したい時に対応するために用意している。使う必要が無いなら使うべきでは無いもの。ただ、突発的にComponentでは対応できない・Componentを作る必要も無いときなんかに使っているユーティリティCSSクラス。
これと似たようなの使っている。
GitHub - Takumi0901/utility-style
recomposeについて
おそらくここ最近で一番気に入っているのがrecompose!Reactのv16.7.0で実装されるHooksもrecomposeの作者が作っているとかなんとかという。
Redux使うとどこのClass Componentでもstateとactionとlifecycleなどが使えるになる。これが後々つらみになるっことが多くて、特にどこでもlifecycle使えるとどこで何やってんだかわけわかんなくなる。Stateの変更をしているのがどこなのか所在がわからなくなったりする。
とはいえ、親Componentからバケツリレーをするにしても階層が深くなればなるほどしんどくなる。
んでrecomposeを使うことによってステートレスComponentのみでComponentを作ることができ、Reduxとのコネクトやlifecycle、ローカルステートを機能として提供することができる。
Atomic Designのorganismsを機能ごとのステートレスComponentにして、機能はenhancerで持つようにしてみた。
そうすることで、見た目と機能を完全に分離することができるのでかなりみやすくなったし、pageごとに切り分けられているorganisms単位になっているので、Stateの変更の所在もそのディレクトリ内で完結している。
ステートレスComponentのみで作ると、pureにすることができずStateの更新が全ての子Componentに波及してしまう。しかし、recomposeではステートレスComponentにpureにしたりonlyUpdateForKeys
でどのStateが更新されたら再renderするか指定することもできる。
これをあるプロダクトで試したところ、体感でもわかるくらいに描画スピードが上がったのでおすすめ。
実際のパフォーマンス向上の施策については下記の記事がとても参考になる。
React製のSPAのパフォーマンスチューニング実例 | リクルートテクノロジーズ メンバーズブログ
終わりに
React v16.7.0でHooksが導入される(2018/10/31現在)
まだすぐにはHooksをプロダクトでは使わないかもしれないが、現状かなり話題になっているのでHooksを利用することにはなると思う。実際、関数型プログラミングが強化されたような感あるし、recomposeっぽいとこもある。今後、React Hooksを使ったボイラープレートも作ろうかと。
ただ、React HooksによりステートレスComponentは今後もっと推奨されるはずなので、ステートレスComponentとロジック部分は切り分けておけば移行もしやすいのではと思っている。
このボイラープレートも無駄では無いはず・・・