react-boilerplate基礎編

# react-boilerplateとは

[オフィシャルサイト](www.reactboilerplate.com)によると

> A highly scalable, offline-first foundation with the best DX and a focus on performance and best practices

**パフォーマンス**と**DX**（デベロッパーエクスペリエンス）重視の*スケーラブル*、*オフラインファースト*な基盤と言ったところでしょうか。

要はプロダクションレベルのreactアプリケーションを工数かけずに立ち上げできるツールですね。

こちらはReact業界ではかの有名な[Max Stoiber](https://github.com/mxstbr)さんを筆頭に、多数の開発者による、開発者のための、オープンソース開発ツールです。

Max StoiberさんによるスケーラブルなReact開発についての[動画](https://vimeo.com/168648012)が公開されているので、興味ある方はぜひご覧ください。

# 特徴

- scaffoldで素早くテンプレを生成できる
- redux/redux-sagaによりステートの管理が楽になる
- オフラインファーストでネット環境が悪くても使用できる
- リンティングとユニットテストがデフォルトでついてくるので初期のセットアップ
が楽

といったところでしょうか。ただし個人的にはルールでガチガチになって後々拡張性に困ってしまうのが心配な方には向いていないと思います。

# 始める前に

こちらはReactをある程度経験している方向けのツールなので、まずは下記の知識について学んでから使いましょう。React初心者は[こちら](https://github.com/petehunt/react-howto)を参考すると良いでしょう。

### コア
- [ ]  [React](https://facebook.github.io/react/)
- [ ]  [React Router](https://github.com/ReactTraining/react-router)
- [ ]  [Redux](http://redux.js.org/)
- [ ]  [Redux Saga](https://redux-saga.github.io/redux-saga/)
- [ ]  [Reselect](https://github.com/reactjs/reselect)
- [ ]  [Immer](https://github.com/mweststrate/immer)
- [ ]  [Styled Components](https://github.com/styled-components/styled-components)

### ユニットテスト
- [ ]  [Jest](http://facebook.github.io/jest/)
- [ ]  [react-testing-library](https://github.com/kentcdodds/react-testing-library)

### リント
- [ ]  [ESLint](http://eslint.org/)
- [ ]  [Prettier](https://prettier.io/)
- [ ]  [stylelint](https://stylelint.io/)

![ワークフロー](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794872/6e055e89-f26d-1e16-45ac-03d41fd97fb0.png)

# 初めてみる

## 依存
まずは以下がセットアップされていることを確認しましょう。

- Node.js v8.15.1以上
- npm v5以上

されていない方は[こちら](https://qiita.com/mk185/items/7ad004bf202f400daea1)を参照してください。

## 初期設定

```sh:root
git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <プロジェクト名>
cd <プロジェクト名>
npm run setup
npm start
```

これで[http://localhost:3000](http://localhost:3000)からサンプルアプリを見れます。

## ジェネレーター
常用する機能を自動で生成する機能です。こちらを使えば大規模な開発でもフォーマットがある程度保たれます。

生成できる機能は**コンテナ**、**コンポーネント**と**言語**です。

Max Stoiberさんは[コンテナ＆コンポーネントのアーキテクチャ](https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.4rmjqneiw)を推していて、コードをタイプ（アクション、スタイルなど）で分けるのではなく、フィーチャーごとに分けるべきという考えです。

大きいカテゴリに分けると

#### コンポーネント：
- データとの連携はしない**見た目重視**のパーツ
- DOMマークアップとスタイルを持つ
- 再利用ができる
- 他の部分への依存がない
- Propsでのみデータとインタラクトする
- 例：ページ、ナビゲーションバー、リスト、ボタン

#### コンテナ：
- **機能（どう動くか）重視**のパーツ
- DOMマークアップとスタイルを持たない（divなどは除く）
- コンポーネントのデータの動作の設定

react-boilerplateはi18nが最初からサポートしているので新しい言語の生成、及びメッセージの設定がとても簡単にできます。

## 新しいコンテナを生成してみる

例えばイベントを宣伝するページを作成するとしましょう。イベントページではおすすめのイベントと全てのイベントのデータを別々にゲットして、ページにレンダーする流れです。

```sh:root
npm run generate container
? What should it be called? EventPage
? Do you want to wrap your component in React.memo? Yes
? Do you want headers? Yes
? Do you want an actions/constants/selectors/reducer tuple for this container? Yes
? Do you want sagas for asynchronous flows? (e.g. fetching data) Yes
? Do you want i18n messages (i.e. will this component use text)? Yes
? Do you want to load resources asynchronously? Yes
```

これで`app/containers/EventPage/`が作成されて、必要なファイルを自動的に繋げてくれます。


### Reduxを設定

#### コンスタンツの定義
まずはReduxのアクションに必要なコンスタンツを設定します。

```javascript:app/containers/EventPage/constants.js
// おすすめイベントのアクション
export const LOAD_FEATURED_EVENTS = 'app/EventPage/LOAD_FEATURED_EVENTS';
export const LOAD_FEATURED_EVENTS_SUCCESS = 'app/EventPage/LOAD_FEATURED_EVENTS_SUCCESS';
export const LOAD_FEATURED_EVENTS_FAILURE = 'app/EventPage/LOAD_FEATURED_EVENTS_FAILURE';

// 全てのイベントのアクション
export const LOAD_EVENTS = 'app/EventPage/LOAD_FEATURED_EVENTS';
export const LOAD_EVENTS_SUCCESS = 'app/EventPage/LOAD_FEATURED_EVENTS_SUCCESS';
export const LOAD_EVENTS_FAILURE = 'app/EventPage/LOAD_FEATURED_EVENTS_FAILURE';
```


#### アクションの定義
```javascript:app/containers/EventPage/actions.js
import {
  LOAD_EVENTS,
  LOAD_EVENTS_SUCCESS,
  LOAD_EVENTS_FAILURE,
  LOAD_FEATURED_EVENTS,
  LOAD_FEATURED_EVENTS_SUCCESS,
  LOAD_FEATURED_EVENTS_FAILURE,
} from './constants';

// おすすめイベントのアクション
export function loadFeaturedEvents(userId, skip, take) {
  return {
    type: LOAD_FEATURED_EVENTS,
    payload: {
      userId,
      skip,
      take,
    },
  };
}

export function loadFeaturedEventsSuccess(featuredEvents) {
  return {
    type: LOAD_FEATURED_EVENTS_SUCCESS,
    payload: {
      featuredEvents,
    },
  };
}

export function loadFeaturedEventsFailure(errors) {
  return {
    type: LOAD_FEATURED_EVENTS_FAILURE,
    payload: {
      errors,
    },
  };
}

// 全てのイベントのアクション
export function loadEvents(userId, skip, take, keyword) {
  return {
    type: LOAD_EVENTS,
    payload: {
      userId,
      skip,
      take,
      keyword,
    },
  };
}

export function loadEventsSuccess(events) {
  return {
    type: LOAD_EVENTS_SUCCESS,
    payload: {
      events,
    },
  };
}

export function loadEventsFailure(errors) {
  return {
    type: LOAD_EVENTS_FAILURE,
    payload: {
      errors,
    },
  };
}

```

#### Reducerの定義
```javascript:app/containers/EventPage/reducers.js
import produce from 'immer';
import {
  LOAD_EVENTS,
  LOAD_EVENTS_SUCCESS,
  LOAD_EVENTS_FAILURE,
  LOAD_FEATURED_EVENTS,
  LOAD_FEATURED_EVENTS_SUCCESS,
  LOAD_FEATURED_EVENTS_FAILURE,
} from './constants';

export const initialState = {
  // イベントのステート
  events: [],
  eventsLoading: false,
  eventsErrors: false,
  // おすすめイベントのステート
  featuredEvents: [],
  featuredEventsLoading: false,
  featuredEventsErrors: false,
};

/* eslint-disable default-case, no-param-reassign */
const eventPageReducer = (state = initialState, action) =>
  produce(state, draft => {
    switch (action.type) {
      case LOAD_EVENTS:
        draft.eventsLoading = true;
        draft.eventsErrors = false;
        break;
      case LOAD_EVENTS_SUCCESS:
        draft.eventsLoading = false;
        draft.eventsErrors = false;
        draft.events = action.payload.events;
        break;
      case LOAD_EVENTS_FAILURE:
        draft.eventsLoading = false;
        draft.eventsErrors = action.payload.errors;
        break;
      case LOAD_FEATURED_EVENTS:
        draft.featuredEventsLoading = true;
        draft.featuredEventsErrors = false;
        break;
      case LOAD_FEATURED_EVENTS_SUCCESS:
        draft.featuredEventsLoading = false;
        draft.featuredEventsErrors = false;
        draft.featuredEvents = action.payload.featuredEvents;
        break;
      case LOAD_FEATURED_EVENTS_FAILURE:
        draft.featuredEventsLoading = false;
        draft.featuredEventsErrors = action.payload.errors;
        break;
    }
  });

export default eventPageReducer;
```

#### Selectorの定義
```javascript:app/containers/EventPage/selectors.js
import { createSelector } from 'reselect';
import { initialState } from './reducer';

/**
 * Direct selector to the eventPage state domain
 */

const selectEventPageDomain = state => state.eventPage || initialState;

/**
 * Featured events selector used by EventPage
 */

const makeFeaturedEventsSelector = () =>
  createSelector(
    selectEventPageDomain,
    substate => substate.get('featuredEvents'),
  );

/**
 * Events selector used by EventPage
 */

const makeEventsSelector = () =>
  createSelector(
    selectEventPageDomain,
    substate => substate.get('events'),
  );

export {
  selectEventPageDomain,
  makeFeaturedEventsSelector,
  makeEventsSelector,
};

```

#### sagaを設定
データをasyncで取得する一番肝の部分になります。今回api連携は主ではないので、モックデータを作ってそこから引っ張りたいと思います。

```sh:root
mkdir app/containers/EventPage/mocks
touch app/containers/EventPage/mocks/events.js
touch app/containers/EventPage/mocks/featuredEvents.js
```

```javascript:app/containers/EventPage/mocks/events.js
// イベントのデータ
const events = [
  {
    id: '1',
    title: 'event 1',
  },
  {
    id: '2',
    title: 'event 2',
  },
  {
    id: '3',
    title: 'event 3',
  },
];

export default events;
```
```javascript:app/containers/EventPage/mocks/featuredEvents.js
// おすすめイベントのデータ
const featuredEvents = [
  {
    id: '1',
    title: 'featured event 1',
  },
  {
    id: '2',
    title: 'featured event 2',
  },
];

export default featuredEvents;
```

それらをsagaにインポートしますが、本番ではfetchの関数内でデータを引っ張ります。

```javascript:app/containers/EventPage/saga.js
import { all, put, takeLatest } from 'redux-saga/effects';
import { LOAD_EVENTS, LOAD_FEATURED_EVENTS } from './constants';
import {
  loadEventsFailure,
  loadEventsSuccess,
  loadFeaturedEventsFailure,
  loadFeaturedEventsSuccess,
} from './actions';
// モックデータを取得
import events from './mocks/events';
import featuredEvents from './mocks/featuredEvents';

function* fetchEvents(action) {
  // ここでイベントのapiからデータを取得
  try {
    yield put(loadEventsSuccess(events));
  } catch (e) {
    yield put(loadEventsFailure(e));
  }
}

function* loadEvents() {
  // takeLatestで一番最新のアクションのみに反応する
  yield takeLatest(LOAD_EVENTS, fetchEvents);
}

function* fetchFeaturedEvents(action) {
  // ここでおすすめイベントのapiからデータを取得
  try {
    yield put(loadFeaturedEventsSuccess(featuredEvents));
  } catch (e) {
    yield put(loadFeaturedEventsFailure(e));
  }
}

function* loadFeaturedEvents() {
  yield takeLatest(LOAD_FEATURED_EVENTS, fetchFeaturedEvents);
}

export default function* eventPageSaga() {
  // 多数のアクションをまとめる
  yield all([loadEvents(), loadFeaturedEvents()]);
}

```

#### index.jsを編集

```diff:app/containers/EventPage/index.js
...

// セレクターとアクションをインポート
- import makeSelectEventPage from './selectors';
+ import { makeEventsSelector, makeFeaturedEventsSelector } from './selectors';
+ import { loadEvents, loadFeaturedEvents } from './actions';

...

// データとディスパッチをプロップとして関数に入れる
 export function EventPage({
+  events,
+  featuredEvents,
+  onLoadEvents,
+  onLoadFeaturedEvents,
 }) {

// 最初のレンダリングと同時にデータをロードする
+  useEffect(() => {
+    onLoadEvents('1111', 10, 15, 'keywords');
+    onLoadFeaturedEvents('1111', 10, 15);
+  }, []);

   return (
     <div>
       <Helmet>
         <title>EventPage</title>
         <meta name="description" content="Description of EventPage" />
       </Helmet>
       <FormattedMessage {...messages.header} />
       {/* データを表示 */}
+      <h4>Featured Events</h4>
+      {featuredEvents.map(e => (
+        <p key={`event-${e.id}`}>{JSON.stringify(e)}</p>
+      ))}
+      <h4>Events</h4>
+      {events.map(e => (
+        <p key={`featuredEvent-${e.id}`}>{JSON.stringify(e)}</p>
+      ))}
     </div>
   );
 }

...

// データとディスパッチのタイプを設定
  EventPage.propTypes = {
+    events: PropTypes.array,
+    featuredEvents: PropTypes.array,
+    onLoadEvents: PropTypes.func,
+    onLoadFeaturedEvents: PropTypes.func,
  };

// データをプロップに注入
  const mapStateToProps = createStructuredSelector({
-    eventPage: makeSelectEventPage(),
+    featuredEvents: makeFeaturedEventsSelector(),
+    events: makeEventsSelector(),
  });

// ディスパッチをプロップに注入
  function mapDispatchToProps(dispatch) {
   return {
-    dispatch,
+    onLoadEvents: (userId, skip, take, keywords) =>
+      dispatch(loadEvents(userId, skip, take, keywords)),
+    onLoadFeaturedEvents: (userId, skip, take) =>
+      dispatch(loadFeaturedEvents(userId, skip, take)),
   };
  }

...

```

#### テストしてみる

まずは新しいページが表示されるように`App`内でルーティングを設定します

```diff:app/containers/App/index.js
+ import EventPage from 'containers/EventPage/Loadable';
  <Switch>
     <Route exact path="/" component={HomePage} />
     <Route path="/features" component={FeaturePage} />
+    <Route path="/events" component={EventPage} />
     <Route path="" component={NotFoundPage} />
  </Switch>
```

これで完了！`npm run start`で[http://localhost:3000/events](http://localhost:3000/events)を開いて確認しましょう！

![Screen Shot 2021-07-15 at 6.24.07 PM.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/794872/7049e33e-57f2-af69-5ffc-bd3289e899f3.png)