【react】ログイン・ログアウトを実装する方法メモ

はじめに

今回やりたかったことはよくあるログイン・ログアウトの実装です。
で、react-routerを使っての実装方法が以下にありました。わかりやすかったです
React Routerで認証を制御する方法 - Qiita

ログインしているかどうかの判定はstoreにuser.sessionを用意してこれに値がセットされていればログインしているとみなす。
本来であればクッキーにtoken等をセットしてそれがあれば自動でログインするような形がいいのかな
今回はとりあえずなので、毎回ログインさせる形で

ディレクトリ構成は以下のような形で実装してみました

.
├── actions
│   └── user.js
├── reducers
│   └── user.js
├── components
│   ├── app.js // 全画面共通のコンポーネント
│   ├── auth.js // ログイン認証用のコンポーネント
│   ├── login.js // ログイン画面
│   ├── welcom.js // トップページ
│   └── member // ログインユーザーのみ見れる画面はこの下に置いてゆく
│        └── top.js
├── reducers.js // 今回はreducerが1個しかないけど、今後増えるときのために全reducer読み込み用のファイル
├── routes.js // ルーティング設定
└── store.js

実装

ルーティング

上記のページにもあったとおりに認証用のコンポーネントを用意してその中に該当のコンポーネントをいれれば実現できるらしい
router.js

import React from "react";
import { Route, IndexRoute } from 'react-router';

import App from "./components/app.js";
import Auth from "./components/auth.js";
import Welcom from "./components/welcom.js";
import Login from "./components/login.js";
import MemberTop from "./components/member/top.js";

const Routes = (
    <Route component={App}>

      { /* ★ログインユーザー専用ここから */ }
      <Route component={Auth}>
        <Route path="member/top" component={MemberTop} />
      </Route>
      { /* ★ログインユーザー専用ここまで */ }

      <Route path="/" component={Welcom} />
      <Route path="login" component={Login} />
    </Route>
);
export default Routes;
コンポーネント

components/app.js(全画面共通の処理とか)

import React from "react";
import { connect } from "react-redux";
import { Link } from "react-router";
import { logout } from "../actions/user.js";

export default class App extends React.Component {

    onLogout(e) {
        e.preventDefault();
        this.props.dispatch(logout());
        this.context.router.push("/");
    }

    render() {
        return (
            <div>
              <ul>
                <li>{ !this.props.user.session ? <Link to="login">Login</Link> : <a href="#" onClick={this.onLogout.bind(this)}>Logout</a> }</li>
                <li><Link to="member/top">MemberTop</Link></li>
              </ul>
              {this.props.children}
            </div>
        );
    }
}

App.contextTypes = {
    router: React.PropTypes.object
};

const connectedApp = connect(state => ({
    user: state.user
}))(App);

export default connectedApp;

全画面共通のヘッダに以下をおく
・ログイン画面へのリンク
・ログアウトボタン
・ログインユーザー専用画面へのリンク

component/auth.js(ログインが必要な画面の共通処理)

import React from "react";
import { connect } from "react-redux";

export default class Auth extends React.Component {

    componentWillMount() {
        this.checkAuth();
    }

    componentWillUpdate() {
        this.checkAuth();
    }

    checkAuth() {
        // ログインしてなければログイン画面へとばす
        if (!this.props.user.session) {
            this.context.router.push("login");
        }
    }

    render() {
        return (
            <div>{this.props.children}</div>
        );
    }
}

Auth.contextTypes = {
    router: React.PropTypes.object
};

const connectedAuth = connect(state => ({
    user: state.user
}))(Auth);

export default connectedAuth;

components/login.js

import React from "react";
import { connect } from "react-redux";
import { login } from "../actions/user.js";

export default class Login extends React.Component {

    constructor(props) {
        super(props);
        this.state = { "message": null };
    }

    componentWillReceiveProps(nextState) {
        if (nextState.user.session) {
            this.context.router.push("member/top");
        } else if (nextState.user.status < 0) {
            this.setState({ "message": "Loginできませんでした" });
        }
    }

    onLogin(e) {
        e.preventDefault();

        var email = this.refs.email.value;
        var password = this.refs.password.value;

        if (email && password) {
            this.setState({ "message": null });
            this.props.dispatch(login(email, password));
        } else {
            this.setState({ "message": "Emailとパスワードを入力してください" });
        }
    }

    render() {
        return (
            <div>
              <div>Login</div>
              <form>
                <span style={{ color: "red" }}>{this.state.message}</span>
                <div><input ref="email" placeholder="email" /></div>
                <div><input ref="password" placeholder="password" /></div>
                <button onClick={this.onLogin.bind(this)}>ログイン</button>
              </form>
            </div>
        );
    }
}
Login.contextTypes = {
    router: React.PropTypes.object
};

const connectedLogin = connect(state => (
    { user: state.user }
))(Login);

export default connectedLogin;
アクション

axiosを使用
非同期通信するときは「redux-thunk」を使う必要があるらしい。
今回も使っているけど通信の話まで入れると内容がそれそうなので今回はこういうものとして・・・

ログイン時にはサーバーサイドのAPIを叩いて以下のようなjsonが返ってくる想定です

// okの場合
array(
    "status" => "OK",
        "result" => array(
        "user" => array(
        "id" => 1,
        "name" => "ユーザーの名前",
      ),
    ),
))

// ngの場合
array(
    "status" => "NG"
)

actions/user.js

import axios from "axios";
import { API_URL } from "../const.js";

export function login(email, password) {
    return dispatch => {
        dispatch(requestLogin());
        axios.get(API_URL + "/login.php?email=" + email + "&password=" + password)
            .then(response => {
                console.log(response.data);
                if (response.data.status == "OK") {
                    dispatch(receiveLoginSuccess(response.data));
                } else {
                    dispatch(receiveLoginFailed());
                }
            })
            .catch(e => {
                dispatch(receiveLoginFailed());
            });
    };
}

export function logout() {
    return { type: "LOGOUT" };
}

// 以下はプライベート関数                                                                                                                                                                                          
function requestLogin() {
    return { type: "LOGIN_REQUEST" };
}

function receiveLoginSuccess(data) {
    return {
        type: "LOGIN_RECEIVE_SUCCESS",
        data: data
    };
}

function receiveLoginFailed() {
    return {
        type: "LOGIN_RECEIVE_FAILED"
    };
}
reducer実装

reducers/user.js

var initialState = {
    session: null, // ログインしたらセットする
    status: 0 //0:処理前 1:ログイン成功 -1:ログイン失敗                                                                                                                                                            
};

export default function login(state = initialState, action) {

    var _state = Object.assign({}, state);
    switch (action.type) {
    case "LOGIN_REQUEST":
        _state.status = 0;
        return _state;

    case "LOGIN_RECEIVE_SUCCESS":
        _state.status = 1;
        _state.session = action.data.result.user;
        return _state;

    case "LOGIN_RECEIVE_FAILED":
        _state.status = -1;
        return _state;

    case "LOGOUT":
        _state.session = null;
        return _state;

    default:
        return state;
    }
}

実装してみてコードを貼り付けただけになってしまったけど・・・とりあえずこれで期待した動きになりました

以上です