Help us understand the problem. What is going on with this article?

TypeScriptで始めるReactプロジェクトのボイラープレート作ってみた

More than 1 year has passed since last update.

あーあ、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とロジック部分は切り分けておけば移行もしやすいのではと思っている。

このボイラープレートも無駄では無いはず・・・

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした