LoginSignup
36
41

More than 3 years have passed since last update.

Reactのreduxを用いたログイン処理周りの実装【初学者のReact✗Railsアプリ開発第4回】

Posted at

やったこと

フロントエンド: React, バックエンド: Rails(APIモード)のアプリ開発における基本的なログイン処理周りの実装
test200111.gif

Rails

まずは、ログイン中のユーザーをjsonとして返すために、userコントローラーを作成します。

$ docker-compose run api rails g controller users
users_controller
module Api
  module V1
    class UsersController < ApplicationController
      before_action :authenticate_api_v1_user!

      def currentuser
        @user = current_api_v1_user
        render json: { status: 'SUCCESS', message: 'Loaded the user', data: @user}
      end

    end
  end
end
  • current_userを使うと、簡単にログイン中のユーザーを返してくれる。ログインしてなかったら、エラーを返す。ルートのパスが/api/v1/user/なので、current_api_v1_userになる。
omniauth_callbacks_controller
          def render_data_or_redirect(message, data, user_data = {})
            ##if Rails.env.production?  コメントアウト!
              if ['inAppBrowser', 'newWindow'].include?(omniauth_window_type)
                render_data(message, user_data.merge(data))


              # 通常、elsif内の処理が実行されるはず。
              elsif auth_origin_url
                redirect_to DeviseTokenAuth::Url.generate(auth_origin_url, data.merge(blank: true))


              else
                fallback_render data[:error] || 'An error occurred'
              end
            else
              # @resource.credentials = auth_hash["credentials"]

              ##render json: @resource, status: :ok コメントアウト!
            end
          end

React

要約

  • Routingでログインが必要なコンポーネントは、Authコンポーネントを経由させて出力させる。
  • Authコンポネートで、未ログイン・ログイン済みを確認して、未ログインならリダイレクト、ログイン済みなら、パスのコンポーネントを表示させた上で、reduxのglobal stateにログイン中ユーザーの情報をセットする。

redux

なんで必要?
-> state管理(状態管理)を楽にするため。reduxが無いと、親から子、子から親などコンポーネント間のデータの受け渡しが必要であるが、reduxを使うと、一つの情報源に全てのコンポーネントが直接アクセスできる。

今回はなぜ使った?
-> ログイン中ユーザーの情報はどのコンポーネントでも使いうるため、"Global State"として、全体で管理したかったから。

App.js

App.js
import React, { Component } from 'react';
import './App.css';
import Home from './containers/Home';
import Term from './containers/Term';
import Info from './containers/Info';
import Auth from './containers/Auth';
import Login from './containers/Login';


import { BrowserRouter, Route, Switch } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <div className="App">
        <BrowserRouter>
          <Switch>
            <Route path="/login" component={Login} />
            <Route path="/info" component={Info} />
            <Route path="/term" component={Term} />
            <Auth>
              <Route exact path="/" component={Home} />
            </Auth>
          </Switch>
        </BrowserRouter>
      </div >
    );
  }
}

export default App;

Login.js

Login.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';


const styles = theme => ({
});

class Login extends React.Component {
  loginTwitter() {
    window.location.href = process.env.REACT_APP_API127_URL + '/api/v1/auth/twitter?auth_origin_url=' + process.env.REACT_APP_BASE_URL;
  }

  render() {
    const { classes } = this.props;
    return (
      <div className={classes.login}>
        <p>未ログイン</p>
        <Button variant="contained" color="secondary" onClick={this.loginTwitter}>
          Twitterで登録ログイン
        </Button>
      </div>
    )
  }
}

Login.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles, { withTheme: true })(Login);

Home.js

Home.js
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const styles = theme => ({
  home: {
    backgroundColor: "red",
    width: "50%"
  },
});

class Home extends React.Component {
  render() {
    const { classes } = this.props;
    return (
      <div className={classes.home}>
        <p>ログイン済み</p>
      </div>
    )
  }
}

Home.propTypes = {
  classes: PropTypes.object.isRequired,
};

export default withStyles(styles, { withTheme: true })(Home);

Auth.js

Auth.js
import React from 'react';
import PropTypes from 'prop-types';

import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import * as actions from '../actions';
import _ from 'lodash';
import { Redirect, Route } from 'react-router-dom'
import "normalize.css";

import queryString from 'query-string';
import axios from 'axios';

const styles = theme => ({
});

class Auth extends React.Component {
  constructor(props) {
    super()
    this.state = {
      isLoading: true,
    }
  }

  componentDidMount() {
    let tokens = queryString.parse(_.get(this, "props.location.search"))
    if (!_.isEmpty(tokens.auth_token)) {
      localStorage.setItem('auth_token', tokens.auth_token)
      localStorage.setItem('client_id', tokens.client_id)
      localStorage.setItem('uid', tokens.uid)
      window.location.href = process.env.REACT_APP_BASE_URL
    } else {
      this.setState({
        isLoading: true,
      })

      const auth_token = localStorage.auth_token
      const client_id = localStorage.client_id
      const uid = localStorage.uid
      axios.get(process.env.REACT_APP_API_URL + '/api/v1/user/currentuser', {
        headers: {
          'access-token': auth_token,
          'client': client_id,
          'uid': uid
        }
      })
        .then((response) => {
          this.setState({
            isLoading: false,
            isLoggedin: true,
          });
          this.props.actions.setCurrentUserSuccess(response.data.data)
        })
        .catch(() => {
          this.setState({
            isLoading: false,
            isLoggedin: false,
          });
        });
    }
  }

  render() {
    const { CurrentUserReducer } = this.props;
    const isLoggedin = this.state.isLoggedin;
    const isLoading = this.state.isLoading;
    const { classes } = this.props;

    console.log(isLoading)

    if (isLoading) {
      return (
        <div>loading</div>
      )
    } else {
      if (isLoggedin) {
        return (
          <Route children={this.props.children} />
        )
      } else {
        console.log(isLoading)
        return (
          <Redirect to={'/login'} />
        )
      }
    }
  }
}

Auth.propTypes = {
  classes: PropTypes.object.isRequired,
};

const mapState = (state, ownProps) => ({
  CurrentUserReducer: state.CurrentUserReducer,
});
function mapDispatch(dispatch) {
  return {
    actions: bindActionCreators(actions, dispatch),
  };
}

export default connect(mapState, mapDispatch)(Auth);
  • このコンポーネントのコードこそが、今回の記事のいちばん重要なポイント。
  • componentDidmount()は、render後に実行される。初めのif文では、ログイン処理を行ってrails側から戻ってきているかどうかを判別。
  • 先程、説明したように、rails側でログイン処理が終わると、devise_token_authでの認証に必要なtokenがクエリとしてURLに乗せられて、/に戻ってきます。if文ではqueryStringというモジュールで、URLの文字列を分析し、?auth_token="..."が存在しているかどうかを判定し、存在していたら、認証に必要なトークンなどをlocalstorageに保存しています。
  • else内では、ログインしているかどうかを判定するために、rails側で先程実装したように、/api/v1/user/currentuserにアクセスして、ログインをしていたら、actionを発行して、reduxを使用して、global stateとして、CurrentUserReducerにログイン中のユーザー情報を保存するようにしています。
  • localのstateとしては、isLoggedin(ログインしているかどうか)とisLoading(currentuserを確認中かどうか)を管理していて、これらを使って、Loadingを表示するか、そのままログイン済みユーザーのみ表示させたいコンポーネントへのアクセスを許可するか、/loginにリダイレクトするかを決めています。
  • Reactのライフサイクルを理解しきっていないため、タイミングの調節が結構苦労しました。

redux

実装の概要

  • index.jsでcreateStoreする、Providerで配る。
  • reducers/rootReducer.jsで複数のreducerを管理している。
  • actions/index.js内で、アクションの内容を記述している。(今回は、setCurrentUserSuccessしか使っていませんが...)
  • reducers/CurrentUserReducer.js内でdispatchされたactionに対するstateの変更の記述をしている。

起こっていること

  1. Auth.js内のthis.props.actions.setCurrentUserSuccess(response.data.data)でactions/index.jsに対して、データが渡されて、actionのオブジェクトがcreateされる。
  2. createされたactionのオブジェクトはreducers/CurrentUserReducer.jsに渡されて、stateの変更が行われる。

index.js

index.js
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import { MuiThemeProvider } from '@material-ui/core/styles';
import { theme } from './materialui/theme'
import { BrowserRouter as Router } from 'react-router-dom';

import createBrowserHistory from 'history/createBrowserHistory';
import { Provider } from 'react-redux';
import rootReducer from './reducers/rootReducer';
import thunk from 'redux-thunk'
import logger from 'redux-logger'
import { createStore, applyMiddleware, compose } from 'redux';


const history = createBrowserHistory();

const store = createStore(
  rootReducer,
  applyMiddleware(thunk, logger)
);

ReactDOM.render(
  <Provider store={store}>
    <MuiThemeProvider theme={theme} >
      <Router>
        <App />
      </Router>
    </MuiThemeProvider>
  </Provider >
  , document.getElementById('root'));

actions/index.js

index.js
import axios from 'axios'

export const setCurrentUser = () => {
  return (dispatch) => {
    const auth_token = localStorage.auth_token
    const client_id = localStorage.client_id
    const uid = localStorage.uid

    return axios.get(process.env.REACT_APP_API_URL + '/api/v1/user/currentuser', {
      headers: {
        'access-token': auth_token,
        'client': client_id,
        'uid': uid
      }
    })
      .then(response => dispatch(setCurrentUserSuccess(response.data.data)))
      .catch(error => dispatch(setCurrentUserFailure(error)))
  };
}

export const setCurrentUserRequest = () => ({
  type: 'SET_CURRENTUSER_REQUEST',
})


export const setCurrentUserSuccess = (json) => ({
  type: 'SET_CURRENTUSER_SUCCESS',
  items: json,
})

export const setCurrentUserFailure = (error) => ({
  type: 'SET_CURRENTUSER_FAILURE',
  items: error,
})

reducers/CurrentUserReducer.js

CurrentUserReducers.js
const initialState = {
  isLoggedin: false,
  isLoading: false,
  items: []
};

const CurrentUserReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'SET_CURRENTUSER_REQUEST':
      return {
        ...state,
        isLoggedin: false,
        isLoading: true,
        items: [],
      };
    case 'SET_CURRENTUSER_SUCCESS':
      if (!action.items) {
        return {
          ...state,
          isLoggedin: false,
          isLoading: false,
          items: action.items,
        };
      } else {
        return {
          ...state,
          isLoggedin: true,
          isLoading: false,
          items: action.items,
        };
      }

    case 'SET_CURRENTUSER_FAILURE':
      return {
        ...state,
        isLoggedin: false,
        isLoading: false,
        error: action.error,
      };
    default:
      return state;
  }
};

export default CurrentUserReducer;

reducers/rootReducer.js

rootReducer.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux'
import CurrentUserReducer from './CurrentUserReducer'

const rootReducer = combineReducers({
  CurrentUserReducer,
  router: routerReducer,
})

export default rootReducer
36
41
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
36
41