React+ReduxアプリにNode.js(express)とJWTで認証・認可周りの処理を実装する②フロント側

## はじめに

[前回](https://qiita.com/rpf-nob/items/1477c3ff8309ec07ddcb)の続きで、今回はReact+Reduxアプリケーションのフロント側の実装の内容を書いていきます。

## 使ったもの

* Node(v12.15.0)
* react(v16.13.1)
* redux(v7.2.0)
* redux-form(v8.3.1)
* redux-thunk(v2.3.0)
* axios(v0.19.2)
* @material-ui/core(v4.9.11)
* @material-ui/icons(v4.9.1)

## ディレクトリ構成

```
root/
├─public
│      index.html
└─src
    │  index.js
    │
    ├─actions
    │      index.js
    │
    ├─components
    │      login.js
    │      privateRoute.js
    │      users.js
    │
    ├─reducers
    │      index.js
    │      login.js
    │      users.js
    │
    └─styles
            style.css
```

## 作るもの

ログイン画面とトップページを作ります。ログイン画面でユーザーIDとパスワードをAPIサーバーにリクエストして、認証したらユーザー情報を取得してトップページにユーザーネームを表示する簡単な実装です。見た目は@material-uiを使って少し調整した程度です。

### ログイン画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619552/2b9dfa8a-20e5-0fd1-427b-70d504a9f6d8.png)

### トップページ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/619552/27c344c5-d70b-e6a7-268d-2d8ab8ddfaaf.png)

## プロジェクト作成
create-react-appでReactのプロジェクトを作成して、必要なパッケージをインストールします。

```
create-react-app app
npm install --save 
```

## RouterとRedux関連の設定
[src/index.js]に基本となるRouterの設定とRedux関連の設定を記述します。
[redux-thunk]は非同期処理を行えるようになるライブラリです。

ログイン画面とトップページのコンポーネントをルートとして設定しています。

```src/index.js
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import { BrowserRouter, Route, Switch } from "react-router-dom";

import "./styles/style.css";
import reducer from "./reducers";

import Login from "./components/login";
import TopPage from "./components/users";

import * as serviceWorker from "./serviceWorker";

const enhancer = applyMiddleware(thunk);
const store = createStore(reducer, enhancer);

ReactDOM.render(
  <Provider store={store}>
    <BrowserRouter>
      <Switch>
        <Route path="/login" component={Login} />
        <Route path="/top" component={TopPage} />
        <Route path="/" component={Login} />
      </Switch>
    </BrowserRouter>
  </Provider>,
  document.getElementById("root")
);

serviceWorker.unregister();

```

## ログイン画面を作る

フォーム部分には[redux-form]を使用しています。バリデーションとかを実装しやすいので使いました。
また、テキストフィールドやボタンは[material-ui]を使用しています。

```src/login.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { Field, reduxForm } from "redux-form";
import { postLogin } from "../actions";

import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";

class Login extends Component {
  constructor(props) {
    super(props);
    this.onSubmit = this.onSubmit.bind(this);
  }
  componentDidMount() {
    localStorage.removeItem("token");
  }
  renderField(field) {
    const {input,label,type,meta: { touched, error },} = field;
    return touched && error ? (
      <TextField error type={type} label={label} defaultValue={label} helperText={error} fullWidth={true} {...input} />
    ) : (
      <TextField label={label} type={type} fullWidth={true} {...input} />
    );
  }

  async onSubmit(values) {
    await this.props.postLogin(values);

    if (this.props.login.isSuccess && this.props.login.token) {
      localStorage.setItem("token", this.props.login.token);
      this.props.history.push("/top");
    } else {
      console.log("Failure Login...");
    }
  }

  render() {
    const { handleSubmit, pristine, submitting, invalid } = this.props;

    return (
      <div className="login__container">
        <h1>Login</h1>
        <form onSubmit={handleSubmit(this.onSubmit)}>
          <div className="login__textfield">
            <Field label="USERID" name="userId" type="text" component={this.renderField} />
          </div>
          <div className="login__textfield">
            <Field
              label="PASSWORD" name="passWord" type="password" component={this.renderField} />
          </div>
          <div className="login__submit">
            <Button type="submit" variant="contained" color="primary" disabled={pristine || submitting || invalid} >
              LOGIN
            </Button>
          </div>
        </form>
      </div>
    );
  }
}

const validate = (values) => {
  const errors = {};

  if (!values.userId) errors.userId = "enter a userID please.";
  if (!values.passWord) errors.passWord = "enter a password please.";

  return errors;
};

const mapStateToProps = (state) => ({ login: state.login });
const mapDispatchToProps = { postLogin };

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(reduxForm({ validate, form: "loginForm" })(Login));

```

### ログインボタン押下時

[mapDispatchToProps]で返ってきた[postLogin]アクションを実行します。
認証に成功したらトークンをローカルストレージに保存し、トップ画面に遷移します。
（ローカルストレージは賛否ありますが、一旦使用しています。）

```javascript
  async onSubmit(values) {
    await this.props.postLogin(values);

    if (this.props.login.isSuccess && this.props.login.token) {
      localStorage.setItem("token", this.props.login.token);
      this.props.history.push("/top");
    } else {
      console.log("Failure Login...");
    }
  }
```

### [postlogin]アクション作成

APIサーバーとの通信は[axios]を使用しています。
フォームから受け取ったIDとパスワードをAPIサーバーに送信します。

```src/actions/index.js
import axios from "axios";

const ROOT_URL = "http://localhost:5000/api/v1";
export const POST_LOGIN = "POST_LOGIN";

export const postLogin = (values) => async (dispatch) => {
  const response = await axios.post(`${ROOT_URL}/login`, values);
  dispatch({ type: POST_LOGIN, response });
};
```

### [login]reducer作成

```src/reducers/login.js
import { POST_LOGIN } from "../actions";

export const login = (events = {}, action) => {
  switch (action.type) {
    case POST_LOGIN:
      return action.response.data;
    default:
      return events;
  }
};
```

### combinereducerで連結

[combineReducers]を使用してreducerを連結します。
[redux-form]もここで連結します。

```src/reducers/index.js
import { combineReducers } from "redux";
import { reducer as form } from "redux-form";
import { login } from "./login";

export default combineReducers({
  login,
  form,
});

```

## トップ画面を作る

画面遷移時にログインしているユーザーの名前を表示する簡単なものです。
[componentDidMount]で[mapDispatchToProps]で返ってきた[getusers]アクションを実行します。
その後[mapStateToProps]で返ってきたusers.nameを画面に表示します。

```src/components/users.js
import React, { Component } from "react";
import { connect } from "react-redux";
import { getUsers } from "../actions";
import { Link } from "react-router-dom";

class TopPage extends Component {
  componentDidMount() {
    this.props.getUsers();
  }

  render() {
    return (
      <React.Fragment>
        <div className="toppage__container">
          <h1>TopPage</h1>
          <p className="toppage__message">Hello {this.props.users.name}!!</p>
          <div className="toppage__navigation">
            <ul>
              <li><Link to="/top">Top</Link></li>
              <li><Link to="/top">DashBoard</Link></li>
              <li><Link to="/top">Profile</Link></li>
              <li><Link to="/top">Contact</Link></li>
            </ul>
          </div>
        </div>
      </React.Fragment>
    );
  }
}

const mapStateToProps = (state) => {
  return {
    users: state.users,
  };
};
const mapDispatchToProps = { getUsers };
export default connect(mapStateToProps, mapDispatchToProps)(TopPage);
```

### [getUsers]アクション作成

トークンをリクエストヘッダーに付与（Authorization: Bearer token のように）して、GETリクエストしてユーザー情報を取得します。
src/actions/index.jsに以下追記してください。

```src/actions/index.js
export const GET_USERS = "GET_USERS";

export const getUsers = () => async (dispatch) => {
  const token = localStorage.getItem("token");
  const response = await axios.get(`${ROOT_URL}/users`, {
    headers: { Authorization: `Bearer ${token}` },
    data: {},
  });
  dispatch({ type: GET_USERS, response });
};

```

### [users]reducer作成

```src/reducers/users.js
import { GET_USERS } from "../actions";

export const users = (events = {}, action) => {
  switch (action.type) {
    case GET_USERS:
      return action.response.data;
    default:
      return events;
  }
};
```

### combinereducerで連結

src/reducers/index.jsに[users]reducer追記してください。

```src/reducers/index.js
import { combineReducers } from "redux";
import { reducer as form } from "redux-form";
import { login } from "./login";
import { users } from "./users";

export default combineReducers({
  login,
  users,
  form,
});

```

## まとめ

これでJWTによるログインとトップページでのユーザー情報取得の実装が終わりです。
ルートを保護する処理で完成となりますが、ちょっと長いので次回にしたいと思います。
何かまずいところあればコメントお願いします。