7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-04-22

はじめに

前回の続きで、今回は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

トップページ

image.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]アクションを実行します。
認証に成功したらトークンをローカルストレージに保存し、トップ画面に遷移します。
(ローカルストレージは賛否ありますが、一旦使用しています。)

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

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?