ExpressサーバーからReactまでのフロントエンドハンズオン 第2章〜フロントエンド編〜

# 概要
今時のフロントエンドってどうやって実装すればいいのか、実際に作りながら説明する
ところどころ省略しているものもあるが、業務運用に耐えうる設計を前提に書いたつもりである

以下の構成で紹介する。この投稿は第2章

- 第１章 [UIサーバー編](https://qiita.com/tyoukan__/items/6888967ee61d7fb59115)
- 第２章 フロントエンド編（ここ）
- 第３章 フロントエンド-redux編 (まだない)

フロントエンド編で扱う、webpackやbabelの設定ファイルは、バージョンが変われば大きく書き方が変わってしまう。インターネットで調べても古いバージョンだとコピペしても動かなかったりするので、公式のドキュメントを参考にするのが一番いい。

主要なパッケージについてここでは以下のバージョンを想定している。

```
babel ^7.1.0
webpack ^4.25.1
eslint ^5.11.0
react ^16.7.0
redux ^4.0.1
@material-ui/core ^3.7.1
```

第１章で作ったUIサーバーが返すJavaScriptファイルを作成するのが目的となる。
1つのHTMLで、SPAページを実装するJavaScriptを作成する

# 作るもの
第１章で作ったUIサーバーが返すHTMLページに埋め込まれる、以下２つのjsファイル(webpackでの出力先)

```
ui/public/js/login.js
ui/public_authenticated/js/app.js
```

github
https://github.com/yas-tyoukan/frontend-basic-sample

# jsがビルドされるまでの流れ
わかってる人は読み飛ばしてよい

JavaScriptファイルは適切に分割して書いたり、パッケージを読み込んだり、ブラウザでは実行できない新しい仕様に基づいた構文で書いたりする。それをブラウザで実行できるjsファイルにするためにビルドする。CSSもjsの中に出力する。

ビルドにはwebpackを使う。
webpackが色々よしなにやってくれる。
（よしなにやってくれるよう設定を書く）

だいたいこんな感じの流れ
<img width="702" alt="スクリーンショット 2018-12-18 16.53.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/c9e20c70-fb6c-6ce8-9792-c5286f5bd373.png">

公式のおしゃれな図： https://webpack.js.org/
<img width="1073" alt="スクリーンショット 2018-12-22 22.52.44.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/d57906f8-29d8-11be-69f3-83dbe09d19b4.png">



# 準備
ディレクトリ構成は[前回](https://qiita.com/tyoukan__/items/6888967ee61d7fb59115)作成したファイルはそのままで、以下のように `src`ディレクトリを追加

```
frontend-sample/
　├ backend/
　└ ui/
  　└ src/
```

必要なパッケージのインストール

以下のpackage.jsonを参考に必要なパッケージをinstallする。
https://github.com/yas-tyoukan/frontend-basic-sample/blob/master/ui/package.json

上記package.jsonファイルを`ui/package.json`に配置して、

```
cd ui/
npm i
```

すればOK

eslintやテストパッケージ(今回はないが)は、開発環境でだけ必要なので`devDependencies`に追加する。
ビルドや本番環境で必要なパッケージは`dependencies`に追加する。


## babelの設定
フロントの実装では新しい構文でJavaScriptを書いたりするが、それをブラウザでも実行できるようにするために変換が必要。
babelはその変換をするためのツール。
構文変換のことを、トランスパイルという。 口語で"バベる"と言うこともある。

どんな風に変換されるかはここで試せる
[Babel · try it out](https://babeljs.io/repl)
<img width="945" alt="スクリーンショット 2019-01-05 18.43.38.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/81eb5754-68bf-321b-2610-f45300d70933.png">


ここではそのbabelの設定を`babel.config.js`に記述する
参考： https://babeljs.io/docs/en/configuration


```js
// ui/babel.config.js
module.exports = function (api) {
  const presets = [
    [
      '@babel/preset-env',
      {
        targets: {
          ie: 11,
        },
        useBuiltIns: 'entry',
      },
    ],
    [
      '@babel/preset-react',
      {
        development: !api.env('production'),
      },
    ],

  ];

  const plugins = [
    '@babel/plugin-proposal-function-bind',
    '@babel/plugin-proposal-export-default-from',
    '@babel/plugin-proposal-export-namespace-from',
    '@babel/plugin-proposal-class-properties',
  ];

  return {
    presets,
    plugins,
  };
};

```
`babel.config.js`というファイル名にしたが、`.babelrc`でも`babel.config.js`でも`.baberc.js`でもいい。

参考： 
[Configure Babel](https://babeljs.io/docs/en/configuration)
[@babel/preset-env](https://babeljs.io/docs/en/babel-preset-env)
[@babel/preset-react](https://babeljs.io/docs/en/babel-preset-react)

以下のような設定を書いている

- IE11で動くようにトランスパイルすること
- Polyfill(後述)する方法は、 `"useBuiltIns": "entry"`を使うこと
  - 参考： https://babeljs.io/docs/en/babel-preset-env#usebuiltins
- NODE_ENVがproductionでない時は、reactのコードについてdevelopmentモードをオンにする 
  - 参考： https://babeljs.io/docs/en/babel-preset-react#development
- プラグインを使って、W3C勧告プロセスで勧告案(proposal)となっているいくつかの記法についてトランスパイルできるようにすること

`Polyfill` とは、 `String.padStart`や`Array.prototype.forEach`など、古いブラウザにはない実装を補完してくれるもの。口語で "ポリフィる" ともいう。

ここではIE11向けにトランスパイルするように設定しているので、IE11にないものだけを補完してくれるようになる。(つまり、IE10以下は切っている)

IE11で動けば他のモダンブラウザでも動くので、chromeやfirefox向けの設定は書いていない。

`useBuiltIns`はポリフィる時の方法の設定。`usage` にすると、実際に使っている必要なものだけにしてくれる。`usage`なら未使用の機能についてはポリフィられないのでビルド後のファイルサイズもわずかに小さくなる。なので`usage`が一番良さそうではあるものの、私が使っていて補完されないケースがあった(補間の必要なメソッド呼び出しをしている変数の型が推定できなくて補完が必要なのかbabelが判断できないケースがあるんじゃないかと推測している)。そのため全部補完するように"entry"にした。


## eslintの設定
フロントの実装をするにあたって、linterとしてeslintを使う。その設定を書く。
airbnbの設定を使用する。 

公式： [ESLint](https://eslint.org/)
eslintでエラーが出て内容がわからない時は、公式で検索するのが速くて正確

ここではついでにUIサーバー側のjsのコードにもeslintの設定を適用できるように書く（UIサーバー側のコードをlintする仕組みはここでは作らないが、eslintrc.jsを作っておけばIDEによってはライブでlintが掛かる)

UIサーバー側のコードに適用するものと、フロント側に適用するもので分けたいので、`ui/.eslintrc.js`と`ui/src/.eslintrc.js`を作成する。

```js
// ui/.eslintrc.js
module.exports = {
  extends: 'airbnb',
};
```

```js
// ui/src/.eslintrc.js
module.exports = {
  env: {
    browser: true,
  },
  parser: 'babel-eslint',
  settings: {
    'import/resolver': {
      webpack: {
        config: 'webpack.config.babel.js',
      }
    },
    react: {
      pragma: 'React',
      version: '16.6'
    },
  },
};
```

eslintの設定ファイルは、プロジェクトルートまでの親ディレクトリをたどるので、`ui/.eslintrc.js`に`airbnb`の設定ファイルを使用することを書いておけば、`ui/src/.eslintrc.js`側には書かなくても`aibnb`の設定ファイルが使用される。

`ui/src/.eslintrc.js`では、ブラウザで使用するコード向けであること、`babel`を使うこと、importする時の解決方法は`webpack.config.babel.js`(後述)の設定を使うこと、reactのバージョンは`16.6`であること、を設定している

## Webpackの設定
Webpackのビルド設定を、`webpack.config.babel.js`に記述する。
Webpackがファイルをビルドする際に、どのファイルをどうやってどこに出力するかの設定を書く。

```js
// ui/webpack.config.babel.js
import path from 'path';
import webpack from 'webpack';

export default (env, args) => {
  const isProduction = args && args.mode === 'production';
  const devtool = !isProduction && 'inline-source-map';
  const sourceMap = !isProduction;
  const rules = [{
    test: /\.jsx?$/,
    exclude: /node_modules/,
    use: 'babel-loader',
  }, {
    test: /\.less$/,
    use: [
      'style-loader',
      {
        loader: 'css-loader',
        options: { sourceMap },
      }, {
        loader: 'less-loader',
        options: {
          sourceMap,
          paths: [
            path.resolve(path.join(__dirname, './src/styles')),
            path.resolve(path.join(__dirname, './src/components')),
          ],
        },
      },
    ],
  }, {
    test: /\.(eot|woff|woff2|ttf|svg|png|jpe?g|gif)(\?\S*)?$/,
    use: [{ loader: 'url-loader', options: { limit: 100000 } }],
  }];

  if (!isProduction) {
    rules.unshift({
      enforce: 'pre',
      test: /\.jsx?$/,
      exclude: /node_modules/,
      loader: 'eslint-loader',
      options: { configFile: path.join(__dirname, './src/.eslintrc.js') },
    });
  }

  return {
    devtool,
    entry: {
      'public/js/login': ['./src/entries/login.jsx'],
      'public_authenticated/js/app': ['./src/entries/app.jsx'],
    },
    output: {
      path: path.join(__dirname, './'),
      filename: '[name].js',
    },
    module: { rules },
    watchOptions: {
      ignored: /node_modules/,
    },
    resolve: {
      modules: ['node_modules'],
      alias: {
        '~': path.join(__dirname, './src'),
      },
      extensions: ['.js', '.jsx', '.css', '.less'],
    },
  };
};

```

`src/entries/login.js`, `src/entries/app.js` をそれぞれ `public/js/login.js`, `public_authenticated/js/app.js` に出力する設定を書いている。

Webpackの設定は関数で記述することができ、実行時の引数を第２引数`args`から取得できる。
`mode`引数の値によって、ソースマップを出力するかを決めたり、環境変数を変えたりしている。

また、 `~`を`./src/`を表すエイリアスとして登録している。これによって、階層の深いjsファイルの`import`文で`../../../`のように書かずに、`~/components/...`のように書けるようにしている。

詳しい内容は、以下参考
基本設定について https://webpack.js.org/concepts/
各loaderについて https://webpack.js.org/loaders/

## 開発で使うscriptを登録
これから、実装を書いていくが、ビルドやウォッチを `npm run ...` で実行できるように、scriptを登録しておく。

`package.json` に以下追記する

```
// package.json (一部) 
  "scripts": {
    /* ... */
    "build": "webpack --mode production",
    "build-dev": "webpack --mode development",
    "watch": "webpack --mode development --watch --progress --colors",
    "server": "node --inspect .",
    "start": "npm run build && node ."
  }
```

フロントエンドのコード実装時には、 `npm run watch`を走らせておき、エラー個所がすぐにリアルタイムでわかるようにしておく

```sh
npm run watch
```

# storybookの設定

storybookはコンポーネントのカタログを作るものであり、開発時のコンポーネント実装のコンポーネント単位で確認するために使う。後から導入してもいいが、コンポーネントの実装をする上で、各コンポーネントの動作確認を簡単にするために最初からあった方が良い。

参考： https://storybook.js.org/

動作イメージは以下のような感じ。
<img width="762" alt="スクリーンショット 2019-01-05 14.52.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/de97a102-bf54-c9ce-28d7-9b3d02d68e22.png">


## インストール
addonも合わせてインストールする（すでにpackage.jsonを使ってinstallした場合は入っているので不要)

```sh
npm i -S @storybook/addon-actions @storybook/addon-notes @storybook/addon-viewport @storybook/cli @storybook/react
```

## storybookの設定
`ui/.storybook`ディレクトリを作成し、以下のファイルを作成する

```js
// ui/.storybook/addons.js
import '@storybook/addon-notes/register';
import '@storybook/addon-actions/register';
import '@storybook/addon-viewport/register';
```

```jsx
// ui/.storybook/config.jsx
import React from 'react';
import {
  addDecorator,
  configure,
  storiesOf,
} from '@storybook/react';
import Wrapper from '~/components/routings/Wrapper';

const WrapperDecorator = storyFn => <Wrapper>{storyFn()}</Wrapper>;

// automatically import all files ending in *.stories.js or *.stories.jsx
const context = require.context('../src/components', true, /\.stories.jsx?$/);

function getDirs(path) {
  return path.replace(/..?\//, '').split('/').reverse().slice(1).reverse();
}

function loadStories() {
  addDecorator(WrapperDecorator);
  context.keys().sort().forEach((c) => {
    const dirs = getDirs(c);

    if (!dirs.length) return;

    const stories = storiesOf(dirs.join('/'), module);
    context(c).default(stories);
  });
}

configure(loadStories, module);
```

細かい説明は省くが、`material-ui`や`redux`、`react-router`を使ってコンポーネントを作成するため、コンポーネントを表示するときに、コンポーネントによっては何かにラップされていないと表示できない・表示が崩れるものがある。(`Link`タグなどは`react-router`の`ConnectedRouter`にラップされていないとエラーになる)

そのため、`addDecorator`を使って表示するコンポーネントをラップする要素`Wrapper`を指定している。`Wrapper`の実装はstorybookのためだけではなく、実装でも使用するため後述する。

また、コンポーネント(`ui/src/components`)以下の`*.stories.jsx`を読み込んで、storybookに載せる設定をしている。

```js
// ui/.storybook/webpack.config.js (一部)
require('@babel/register');
const config = require('../webpack.config.babel.js');

module.exports = config.default(process.env, { mode: process.env.NODE_ENV });
```

`ui/.storybook/static` にstorybookで静的にホストしたいファイルなどがあれば置いておく（画像など)
ここでは画像を使うコンポーネントを作らないので、適当にfaviconでも置いておく

<img width="307" alt="スクリーンショット 2019-01-05 19.06.19.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/7fd7da62-6d6e-3c94-bf11-c8825671fa6d.png">

## scriptの登録
storybookを`npm run`を使って立ち上げられるように`package.json`にscriptを登録しておく

```json
// ui/package.json (一部)
    "storybook": "start-storybook -s ./.storybook/static -p 6006",
    "build-storybook": "build-storybook -s ./.storybook/static"
```

`build-storybook`は`index.html`含めた静的ファイルを出力する。`storybook`はサーバーが立ち上がる。

`npm run storybook`でサーバーを立ち上がると、ブラウザにstorybookのページが開く。

# ログイン画面の作成

各コンポーネントはatmic designに則って作成する。データバインドや状態管理は`redux`, `react-redux`を使用する

参考 [コンポーネント指向でreact-reduxで画面を作るための考え方](https://qiita.com/tyoukan__/items/2467edbcae4720def86b)

ディレクトリ構成はこんな感じ
<img width="321" alt="スクリーンショット 2019-01-05 15.18.39.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/f66f2773-52b8-2c02-b413-2b5d2465dc26.png">

よく紹介されているreact-reduxの構成を少し自分でカスタマイズしたものである。
参考: [A Better File Structure For React/Redux Applications](https://marmelab.com/blog/2015/12/17/react-directory-structure.html)
上の日本語紹介記事:  [React+Reduxのディレクトリ構成検討](https://bmf-tech.com/posts/React+Redux%E3%81%AE%E3%83%86%E3%82%99%E3%82%A3%E3%83%AC%E3%82%AF%E3%83%88%E3%83%AA%E6%A7%8B%E6%88%90%E6%A4%9C%E8%A8%8E)

以下の用途を意図している

- `ui/src`以下にwebpackでビルドする対象のコードを配置
- `ui/src/components`以下にコンポーネントを配置
- `ui/src/components/routings`以下に、ルーティングを担うコンポーネント実装の配置
- `ui/src/form`以下にフォーム(`redux-form`)で使うバリデーション実装やsubmit処理実装のファイル配置
- `ui/src/entries`以下に、ビルドの起点となるファイルを配置

## 各ファイルの実装
以下、必要なパッケージのinstallコマンドの記載は省く。先に紹介した[package.json](https://github.com/yas-tyoukan/frontend-basic-sample/blob/master/ui/package.json)を使ってinstallした場合は入っているので都度のインストールは不要。必要なものだけ入れたい場合はコード中のimport文を見て足りないパッケージをinstallするなどで対応すること。

```jsx
// ui/src/entries/login.jsx
import '@babel/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';

import Wrapper from '~/components/routings/Wrapper';
import * as rootReducer from '~/reducers/login';
import Login from '~/components/routings/Login';

ReactDOM.render(
  <Wrapper rootReducer={rootReducer}>
    <Login />
  </Wrapper>,
  document.getElementById('root'),
);
```

ログインページのreducerの記述。ログイン後と分けるために、`ui/src/reducers/login`ディレクトリを作成して、その中に実装する

```js
// ui/src/reducers/login/index.js
// ログイン画面で使用するreducer
export login from './login';
```

```js
// ui/src/reducers/login/login.js
import { actionTypes } from 'redux-form';

const initialState = {
  error: null,
  succeed: false,
};

export default function login(state = initialState, action) {
  switch (action.type) {
    case actionTypes.SET_SUBMIT_SUCCEEDED:
      return { ...state, succeed: true };
    case actionTypes.SET_SUBMIT_FAILED:
      return { ...state, error: action.error };
    default:
      return state;
  }
}

```

ログイン前のページのルーティングが入ったコンポーネントをレンダリングするのが`ui/src/entries/login.jsx`の役割である。ここで使っている`Wrapper`は、ログイン後も共通のものとなる、ルーティング周りの実装を書いている。

```jsx
// ui/components/routings/Wrapper/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Provider } from 'react-redux';
import { createBrowserHistory } from 'history';
import { ConnectedRouter } from 'connected-react-router';
import { MuiThemeProvider } from '@material-ui/core';

import '~/styles/main.less';
import theme from '~/styles/theme';
import init from '~/utils/init';
import configureStore from '~/configureStore';

const Wrapper = ({ rootReducer, children }) => {
  const history = createBrowserHistory();
  const store = configureStore({}, rootReducer, history);
  init(store.dispatch);
  return (
    <MuiThemeProvider theme={theme}>
      <Provider store={store}>
        <ConnectedRouter history={history}>
          {children}
        </ConnectedRouter>
      </Provider>
    </MuiThemeProvider>
  );
};

Wrapper.propTypes = {
  rootReducer: PropTypes.shape({}),
  children: PropTypes.node,
};

Wrapper.defaultProps = {
  rootReducer: {},
  children: null,
};

export default Wrapper;
```
`import '~/styles/main.less';` としているのは、ここでコンポーネントによらない共通のスタイル定義を読み込んでいる。 `html,body{width:100%;height:100%;}`のような共通のスタイルや、変数宣言などが含まれる。この後作成する、ログイン後の画面でも共通としている。

`import theme from '~/styles/theme';` で読み込んでいるテーマファイルは、`MUIThemeProvider`に渡しているテーマを設定している。`material-ui`の部品やスタイルを使うため、`MUIThemeProvider`でコンポーネント全体をラップしている。

参考： Material-UI https://material-ui.com/
参考： MUIThemeProvider API https://material-ui.com/api/mui-theme-provider/

`ui/src/styles`以下の実装は、ここでは省略とする。リポジトリを参照のこと。 
https://github.com/yas-tyoukan/frontend-basic-sample/tree/master/ui/src/styles

次に初期設定の処理の記述

```js
// ui/src/utils/init.js
import axios from 'axios';

import { ajaxError } from '~/actions/common';

/**
 * 初期設定
 * @param {Function} dispatch dispatch関数
 */
export default (dispatch) => {
  /* ---------- axiosの設定 ---------- */
  axios.defaults.responseType = 'json';
  axios.interceptors.request.use((config) => {
    if (!(config.data && config.data.headers && config.data.headers['csrf-token'])) {
      // すでに設定されている場合をのぞいて、csrf-tokenをmetaタグから取得して設定する
      // eslint-disable-next-line no-param-reassign
      config.headers['csrf-token'] = document.querySelector('meta[name=csrf-token]').getAttribute('content');
    }
    return config;
  });
  // 共通のエラーハンドラ定義
  axios.interceptors.response.use(
    res => res,
    (error) => {
      dispatch(ajaxError(error));
      return Promise.reject(error);
    },
  );
};
```

```js
// ui/src/actions/common/index.js
import {
  CLEAR_ERROR,
  ERR_AJAX,
} from '../actionTypes/common';

export const ajaxError = error => ({
  type: ERR_AJAX,
  error,
});

export const clearError = () => ({
  type: CLEAR_ERROR,
});
```

```js
// ui/src/actionTypes/common/index.js
export const ERR_AJAX = 'ERR_AJAX';
export const CLEAR_ERROR = 'CLEAR_ERROR';
```

`ui/src/utils/init.js`では初期設定をしている。ここでは`axios`を使った時の初期設定と、失敗時にactionを実行する設定を行なっている。


次にreduxのconfigureStoreの設定

```js
// ui/src/configureStore.js
import {
  applyMiddleware,
  combineReducers,
  createStore,
} from 'redux';
import { reducer as formReducer } from 'redux-form';
import { routerMiddleware } from 'react-router-redux';
import { connectRouter } from 'connected-react-router';
import thunkMiddleware from 'redux-thunk';

const middleware = [thunkMiddleware];

if (process.env.NODE_ENV !== 'production') {
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
  const reduxLogger = require('redux-logger');
  middleware.push(reduxLogger.createLogger());
}

export default function configureStore(initialState, reducers, history) {
  middleware.push(routerMiddleware(history));

  if (history) {
    middleware.push(routerMiddleware(history));
  }
  return createStore(
    combineReducers({ ...reducers, form: formReducer, router: connectRouter(history) }),
    initialState,
    applyMiddleware(...middleware),
  );
}
```

参考： [Configuring Your Store](https://redux.js.org/recipes/configuring-your-store)

`redux-logger`は開発時だけ使いたくて、プロダクションビルド時は含めないように実装している。

`if (process.env.NODE_ENV !== 'production') {`としているのは、知らないと違和感を感じるかもしれない。なぜならこのコードはフロントエンドのコードで、ビルドするといろんなブラウザが実行するものなのに、`process.env.NODE_ENV`はビルド時の環境変数を参照しているような実装だからである。

これは、webpackでビルドする時にwebpackが設定してくれる値を参照している。webpackを使ってビルドするときに、指定したmodeによって、`process.env.NODE_ENV`の値が変わる。

参考： https://webpack.js.org/concepts/mode/#mode-development

これによって、webpackを`mode=production`で実行すると、`process.env.NODE_ENV`が`"production"`になり、該当の部分は、`if('production'!=='production') {` となり、minifyされるので、`if(false){}`と解釈され、if文内の処理はビルドするコードに含まれない、結果、productionの場合は`redux-logger`は適用もされず、`require`もされない、という仕組みである。

次に、ログインページのルーティングの設定

```jsx
// ui/src/components/routings/Login
import React from 'react';
import {
  Redirect,
  Route,
  Switch,
} from 'react-router';
import { Link } from 'react-router-dom';
import Login from '~/components/pages/Login';

export default () => (
  <Switch>
    <Redirect exact from="/" to="/login" />
    <Route exact path="/login">
      <Login />
    </Route>
    <Route exact path="/login/password-reminder">
      <>
        <div>id/pass = user1/p</div>
        <Link to={{ pathname: 'login' }}>ログインに戻る</Link>
      </>
    </Route>
    <Redirect from="*" to="/login" />
  </Switch>
);
```

`/login`と`/login/password-reminder`のルーティングを設定している。ルーティングの動作確認のためにパスワードリマインダーを作っているだけなので、実装は簡易的なものにしている。

ここからコンポーネントの実装。

```jsx
// ui/src/components/Login/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';

import submitLogin from '~/form/submit/login';
import CenteringTemplate from '~/components/templates/CenteringTemplate';
import LoginForm from '~/components/organisms/LoginForm';

const onSubmitSuccessDefault = () => {
  window.location.href = '/';
};

export default class Login extends React.PureComponent {
  static propTypes = {
    onSubmit: PropTypes.func,
    onSubmitSuccess: PropTypes.func,
  };

  static defaultProps = {
    onSubmit: submitLogin,
    onSubmitSuccess: onSubmitSuccessDefault,
  };

  constructor() {
    super();
    this.onSubmit = ::this.onSubmit;
    this.onSubmitSuccess = ::this.onSubmitSuccess;
  }

  onSubmit(values) {
    const { onSubmit } = this.props;
    if (onSubmit) {
      return onSubmit(values);
    }
    return null;
  }

  onSubmitSuccess() {
    const { onSubmitSuccess } = this.props;
    if (onSubmitSuccess) {
      return onSubmitSuccess();
    }
    return null;
  }

  render() {
    const contentsEl = (
      <>
        <h1>ログイン</h1>
        <LoginForm
          onSubmit={this.onSubmit}
          onSubmitSuccess={this.onSubmitSuccess}
        />
        <Link to={{ pathname: 'login/password-reminder' }}>パスワードを忘れた方はこちら</Link>
      </>
    );
    return (
      <CenteringTemplate
        vertical
        horizontal
        className="p_login"
        contents={contentsEl}
      />
    );
  }
}
```

storybookに載せるstoryも作成する

```jsx
// ui/src/componens/pages/Login/index.stories.jsx
import React from 'react';
import { action } from '@storybook/addon-actions';
import Login from '.';

export default stories => stories
  .add('default', () => (
    <Login
      onSubmit={action('onSubmit')}
      onSubmitSuccess={action('onSubmitSuccess')}
    />
  ));
```

submit関数

```js
// ui/src/form/submit/login.js
import axios from 'axios';
import { SubmissionError } from 'redux-form';

export default function onSubmit(values) {
  // CSRF対策トークンを取得してからログインのPOSTを行う
  return axios.get('/csrf-token')
    .then((res) => {
      document.querySelector('meta[name=csrf-token]').setAttribute('content', res.data.token);
    })
    .then(() => axios.post('/api/login', values))
    .catch(() => {
      throw new SubmissionError({ _error: 'authenticated failure' });
    });
}
```

テンプレートコンポーネントの作成。中央に1つのコンポーネントを配置するテンプレートとして作成している。propsで左右中央か、上下中央か（その両方か）を指定できるように作っている

```jsx
// ui/src/components/templates/CenteringTemplate/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import './style.less';

const CenteringTemplate = ({
  className,
  contents,
  horizontal,
  vertical,
}) => (
  <div className={classNames('t_centering-template', className, { horizontal, vertical })}>
    <div className="contents">{contents}</div>
  </div>
);

CenteringTemplate.propTypes = {
  className: PropTypes.string,
  contents: PropTypes.node,
  horizontal: PropTypes.bool,
  vertical: PropTypes.bool,
};

CenteringTemplate.defaultProps = {
  className: '',
  contents: '',
  horizontal: false,
  vertical: false,
};

export default CenteringTemplate;
```

中央寄せのためのスタイル定義

```scss
// ui/src/components/templates/CenteringTemplate/style.less
.t_centering-template {
  height: 100%;
  display: flex;
  flex-direction: column;

  &.vertical {
    justify-content: center;
  }

  &.horizontal {
    align-items: center;
  }
}
```

story

```jsx
import React from 'react';
import CenteringTemplate from '.';

const contentsSampleEl = (
  <>
    <h1>問題</h1>
    <p>『エビフライ』にあって、『カキフライ』にないもの、なぁ〜んだ？</p>
    <p>答え：エビ</p>
  </>
);

export default stories => stories
  .add('horizontal', () => <CenteringTemplate horizontal contents={contentsSampleEl} />)
  .add('vertical', () => <CenteringTemplate vertical contents={contentsSampleEl} />)
  .add('horizontal and vertical', () => <CenteringTemplate horizontal vertical contents={contentsSampleEl} />);
```

LoginForm

```jsx
// ui/src/components/organisms/LoginForm/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import {
  Field,
  propTypes as reduxFormPropTypes,
  reduxForm,
} from 'redux-form';
import './style.less';
import validate from '~/form/validates/login';
import TextField from '~/components/atoms/TextField';
import Button from '~/components/atoms/Button';

export const Form = ({
  onSubmit,
  handleSubmit,
  submitFailed,
  submitting,
  error,
}) => (
  <form
    onSubmit={handleSubmit(onSubmit)}
    className="o_login-form"
  >
    {(submitFailed && error)
      ? (
        <div className="error-box">
          <div className="messageWrapper">
            <p className="message">
              ログインIDまたはパスワードが違います。
            </p>
            <p className="message">
              入力内容に誤りがないか ご確認下さい。
            </p>
          </div>
        </div>
      )
      : ''
    }
    <Field
      name="id"
      type="text"
      component={TextField}
      placeholder="ログインID"
      fullWidth
    />
    <Field
      name="password"
      type="password"
      component={TextField}
      placeholder="パスワード"
      fullWidth
    />
    <Button type="submit" size="large" disabled={submitting} fullWidth>
      ログイン
    </Button>
  </form>
);

Form.propTypes = {
  ...reduxFormPropTypes,
  onSubmit: PropTypes.func.isRequired,
};

export default reduxForm({
  form: 'login',
  validate,
})(Form);
```

storyの作成

```jsx
// ui/src/components/organisms/LoginForm/index.stories.jsx
import React from 'react';
import { createDecoratedForm } from '~/components/story_utils';
import { Form } from '.';

export default stories => stories
  .add('default', () => {
    const LoginForm = createDecoratedForm('login', Form);
    return <LoginForm />;
  })
  .add('ログイン失敗', () => {
    const LoginForm = createDecoratedForm('login', Form, { submitFailed: true, error: { _error: 'error' } });
    return <LoginForm />;
  });

```

フォーム内で使っている`Field`コンポーネントは、reduxと繋がった部品が親にないとエラーになるため、フォームのコンポーネントをよしなにして、Fieldを含むフォームを表示できるようにするutilsを作る。storybookでコンポーネントを表示したいだけなのに、`redux-form`を使っているためにreduxのstateのことも考えなくてはいけないのは嫌な感じがするので、別案があれば改善したいところであるが、一旦utilsを作って対応している。

```jsx
// ui/src/components/story_utils.jsx
import React from 'react';
import {
  combineReducers,
  createStore,
} from 'redux';
import { Provider } from 'react-redux';
import {
  reducer,
  reduxForm,
} from 'redux-form';
import { action } from '@storybook/addon-actions';

export const actions = {
  onSubmit(e) {
    e.preventDefault();
    action('onSubmit')(e);
  },
  handleSubmit(arg) {
    if (typeof arg.preventDefault === 'function') {
      arg.preventDefault();
    }
    return arg;
  },
};

export function createDecoratedForm(form, FormComponent, formState) {
  const Decorated = reduxForm({ form })(FormComponent);
  const mockStore = createStore(
    combineReducers({ form: reducer }),
    { form: { [form]: formState } },
  );
  return props => (
    <Provider store={mockStore}>
      <Decorated {...actions} {...props} />
    </Provider>
  );
}

export const TestForm = createDecoratedForm('sample', 'form');
```

validationの実装

```js
// ui/src/form/validates/login.js
export default (values) => {
  const errors = {};
  if (!values.id) {
    errors.id = '必須項目です';
  }
  if (!values.password) {
    errors.password = '必須項目です';
  }
  return errors;
};
```

styleの実装

```scss
// ui/src/components/organisms/LoginForm/style.less
.o_login-form {
  > .a_text-field,
  > .a_button {
    display: inline-block;
    margin-top: 2em;
  }

  > .a_button {
    height: 5em;
  }

  > .error-box {
    padding-top: 1em;
  }
}
```

TextFieldの実装(Material-UIのラップ)

```jsx
// ui/src/components/atoms/TextField/index.jsx
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import MUITextField from '@material-ui/core/TextField';

export const TextFieldContainer = ({
  error,
  meta = {},
  helperText,
  presenter,
  input: inputProps,
  className,
  ...remainProps
}) => {
  const hasError = !!(error || (meta && meta.error && meta.touched));
  const helperTextOrErrorText = hasError && (meta.error || helperText);
  return presenter({
    ...remainProps,
    inputProps,
    className: classNames('a_text-field', className),
    error: hasError,
    helperText: helperTextOrErrorText,
  });
};

export const TextFieldPresenter = ({ ...props }) => <MUITextField {...props} />;

/**
 * https://material-ui.com/api/text-field/
 * @param props
 * @returns {*}
 * @constructor
 */
const TextField = ({ ...props }) => (
  <TextFieldContainer {...props} presenter={TextFieldPresenter} />
);

TextField.propTypes = {
  variant: PropTypes.oneOf(['standard', 'outlined', 'filled']),
};

TextField.defaultProps = {
  variant: 'standard',
};

export default TextField;
```

`TextField`は`redux-form`の`Field`の`component`プロパティの値として使用するため、`redux-form`が`meta`などの固有のプロパティを渡す。それをよしなに変換するロジックを実装している。ロジック部分を、`TextFieldContainer`、表示部分を`TextFieldPresenter`にして、それらを合わせたものを`TextField`として作成している。

こうすることで、例えば`redux-form`を使うのをやめたり、`redux-form`の仕様が変わった場合でも、表示するコンポーネントは変えずにロジック部分=Containerコンポーネントの変更に留まるというメリットがある。ここでは作らないが、例えばチェックボックスを考えると見た目は２種類しかないのに、状態管理などは複雑になるため、見た目だけのPresenterコンポーネントと、ロジックを実装するContainerコンポーネントに分けるメリットが大きい。ContainerコンポーネントはReact関係ない純粋なjsなので、テストも書きやすい。

story

```jsx
// ui/src/components/atoms/TextField/index.stories.jsx
import React from 'react';
import { action } from '@storybook/addon-actions';
import TextField from '.';

const onChange = action('onChange');

export default stories => stories
  .add('default', () => (
    <TextField
      placeholder="ログインID"
      label="ログインID"
      input={{ onChange }}
      meta={{}}
    />
  ))
  .add('error', () => (
    <TextField
      placeholder="ログインID"
      label="ログインID"
      input={{ onChange }}
      error
      helperText="入力に間違いがあります"
    />
  ))
  .add('meta.error (redux-form想定)', () => (
    <TextField
      placeholder="ログインID"
      label="ログインID"
      input={{ onChange }}
      error
      meta={{ error: '入力に間違いがあります' }}
    />
  ));
```

次にButtonコンポーネント

```jsx
// ui/src/components/atoms/Button/index.jsx
import React from 'react';
import classNames from 'classnames';
import MUIButton from '@material-ui/core/Button';

export default ({ className, ...props }) => (
  <MUIButton className={classNames('a_button', className)} {...props} />
);
```

story

```jsx
// ui/src/components/atoms/Button/index.stories.jsx
import React from 'react';
import { action } from '@storybook/addon-actions';
import Button from '.';

export default (stories) => {
  const props = {
    onClick: action('onClick'),
  };
  const colors = [
    'default',
    'inherit',
    'primary',
    'secondary',
  ];

  const sizes = [
    'small',
    'medium',
    'large',
  ];

  const variants = [
    'text',
    'outlined',
    'contained',
  ];

  colors.forEach(color => stories
    .add(`color: ${color}`, () => (<Button color={color} {...props}>ボタン</Button>)));

  sizes.forEach(size => stories
    .add(`size: ${size}`, () => (<Button size={size} color="primary" {...props}>ボタン</Button>)));

  variants.forEach(variant => stories
    .add(`variant: ${variant}`, () => (<Button variant={variant} color="primary" {...props}>ボタン</Button>)));

  stories
    .add('multi-line', () => (
      <Button
        color="primary"
        {...props}
      >
        <p>
          利用規約に同意して
          <br />
          登録する
        </p>
      </Button>
    ));

  stories
    .add('fullWidth', () => (<Button color="primary" fullWidth>ボタン</Button>));

  stories
    .add('disabled', () => (<Button color="primary" disabled>ボタン</Button>));

  return stories;
};
```

ここまでできたらログイン画面の完成である。すなわち、`ui/public/js/login.js`へビルドするファイルが揃い、`localhost:3000`へアクセスすると第１章で作成したexpressサーバーが、ビルド後のファイル`ui/public/js/login.js`をホストし、ログイン画面が表示される。


# 動作確認

動作確認のためにログイン後の画面も一旦ダミーで作成しておこう

```jsx
// ui/src/components/entries/app.jsx
import '@babel/polyfill';
import React from 'react';
import ReactDOM from 'react-dom';

import Wrapper from '~/components/routings/Wrapper';

ReactDOM.render(
  <Wrapper rootReducer={{}}>
    <span>ようこそ</span>
  </Wrapper>,
  document.getElementById('root'),
);
```

## コンポーネントの確認
作ったコンポーネントが正しく表示されるかをstorybookを立ち上げて確認する

```
cd ui/
npm run storybook
```

localhost:6006
<img width="1082" alt="スクリーンショット 2019-01-05 19.16.35.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/6ac43fba-41eb-1e0e-4089-2da77b69f9a6.png">



## ログイン画面の確認

第１章で作成した、ダミーのバックエンドと、expressサーバー(UIサーバー)を立ち上げる

```
cd backend/
npm run start
```

```
cd ui/
npm run server
```

フロント側のjsのビルド(watch)

```
cd ui/
npm run watch
```

localhost:3000にアクセス

<img width="604" alt="スクリーンショット 2019-01-05 17.57.22.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/91aae3a2-a5fd-d512-1ecc-3a4065a3362b.png">


パスワードリマインダーページ

<img width="391" alt="スクリーンショット 2019-01-05 17.57.30.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/e716fb9a-2f51-45b6-0d48-7e385421ce55.png">

ログイン成功時のページ（ダミー）

<img width="350" alt="スクリーンショット 2019-01-05 17.56.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/71390/c5e698ce-25ef-51e1-461f-0c73a2b26b19.png">

## 書いてある通りにやったのに動かない場合
説明に漏れがあるかもしれません。コメントいただければレスします。


# 終わりに

この後は、ログイン成功時に表示されるページで、主に`redux`、`redux-thunk`を使った、非同期通信、データバインドの実装をやっていく予定。

githubのリポジトリでは実装済みなので、待ちきれない方はそちらを参考にしてください。

github
https://github.com/yas-tyoukan/frontend-basic-sample