Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
51
Help us understand the problem. What is going on with this article?
@kurawo___D

React Hooks + Rails APIを使ったログイン・認証系機能の実装手順

はじめに

Rails(API)とReact Hooksを使って新規登録/ログイン/ログアウト機能を実装してみました。

Rails側ではDeviseなどのライブラリは使わずにセッションを使って実装しています。
セッションメソッドなどについて知る際に役に立つ公式ドキュメント:Railsガイド(セッション)

参考にさせて頂いた資料はReact + Rails API Authenticationというedutechionalさんの動画です。(こちらの動画ではReact側はクラスコンポーネントを用いた方法を解説してくださっていました。)

環境

RubyやRailsや、Reactアプリ作成に必要なyarnまたはnpmのインストールなどの環境構築は省きます。

Rails側
 ・Ruby 2.6
 ・Rails 5.2
 ・bcrypt
 ・rack-cors

React側
 ・create-react-app
 ・axios
 ・react-router-dom
 ※React HooksはReact16.8以降でしか使えないので注意です

実装手順

1.Rails側の実装
 ・Railsアプリケーションのセットアップ
 ・CORSの設定
 ・cookiesを正しく処理するための設定
 ・Pumaの設定
 ・Userモデルの作成とbcryptの設定
 ・ルーティングの設定
 ・コントローラの作成とその他の設定

2.React側の実装
 ・Reactアプリケーションのセットアップ
 ・react-router-domでルーティングを設定する
 ・新規登録コンポーネントを実装する
 ・ログイン状態をチェックする機能を実装する
 ・ログインコンポーネントを実装する
 ・ログイン状態を維持する機能を実装する
 ・ログアウト機能を実装する

1.Rails側の実装

1-1.Railsアプリケーションのセットアップ

  • Rails newコマンドを実行する。DBはPostgreSQLを使います。
$ rails new auth-app-api --database=postgresql
  • rails db:createコマンドを実行し、DBを立ち上げる。
$ rails db:create
  • Gemfileにbcryptとrack-corsを追加し、インストールする。
Gemfile
gem 'bcrypt'

gem 'rack-cors'

rack-corsは、CORSの設定を行うために用います。CORSについては後述します。

$ bundle


CORS(Cross-Origin Resource Sharing)の設定

通常、JavaScript Webアプリ(1つのオリジン)がRails API(別のオリジン)のリソースにアクセスすること(GET, POST, PUT, DELETE などのHTTPリクエスト)は制限されています。

なので僕らのReactアプリが、Rails APIにリクエストを送信できるようにするためにCORSを有効にする必要があります。

CORSとはなんぞやと言う方は以下の記事をまず眺めてみてから、「CORS」で色々とググってみると良いかなと思います。
オリジン間リソース共有 (CORS) - MDN web docs

そんなCORSの設定を以下で行います。

  • まずはconfig/initializersディレクトリ内に、 新たにcors.rbファイルを作成し、以下のようにコードを記述します。
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
    allow do
        origins 'http://localhost:3000'

        resource '*',
            headers: :any,
            methods: [:get, :post, :put, :patch, :delete, :options, :head],
            credentials: true
    end

    #本番環境用のオリジン設定
    allow do
        origins 'https:<自身が設定するアプリのURL>'

        resource '*',
            headers: :any,
            methods: [:get, :post, :put, :patch, :delete, :options, :head],
            credentials: true
    end
end

上記のコードをざっくり説明しますと、allow ... doに囲まれたオリジンからの色んなリクエストを許可するみたいなことです。

originsでフロントエンド側のオリジン(URL)を指定し、
resource「許可するヘッダ」「許可するHTTPリクエスト」「Cookie保持を許可するかどうか」を指定します。

この設定により、localhost:3000オリジン(Reactアプリ側)からの全てのタイプのヘッダやリクエストの許可や、Cookieの保持がRails側で許可されるようになりました。


Cookieを正しく処理するための設定

  • config/initializers/session_store.rbファイルを作成し、サーバーがHTTP Cookieを処理できるように設定を行う。
config/initializers/session_store.rb
if Rails.env === 'production'
    Rails.application.config.session_store :cookie_store, key: '_auth-app-api', domain: 'フロントエンドのドメイン'
else
    Rails.application.config.session_store :cookie_store, key: '_auth-app-api'
end

サーバーはCookieについて、少なくともkeyについて知らせる必要があります。(keyはアンダースコアから始めることに注意です。)

keyを指定すると、ブラウザ側には「Set-Cookieヘッダ」として、以下のような形でCookieが保持されることになります。

Set-Cookie
_auth-app-api=59gajghl89jgaji5l.......

domainについては本番環境へ移行する際に必要になります。


Pumaの設定

  • config/puma.rbファイルを開いて、デフォルトポートを3001に変更する。
config/puma.rb
-  port        ENV.fetch("PORT") { 3000 }
+  port        ENV.fetch("PORT") { 3001 }

上記の設定により、Railsのローカルサーバーが3001番ポートで開くようになります。

これを設定することにより、バックエンドとフロントエンドが同じポート番号でサーバーを開こうとするのを防ぐことができます。(デフォルトではReactもRailsも3000で開くので要設定)


Userモデルの作成とbcryptの設定

  • Userモデルを作成し、内容をデータベースへ保存。
$ rails g model User email password_digest
$ rails db:migrate

passwordは必ずpassword_digestとしてください。

  • user.rbを開き、Userモデルとbcryptの設定をします。
app/models/user.rb
class User < ApplicationRecord
    has_secure_password

    validates :email, presence: true
    validates :email, uniqueness: true
    validates_format_of :email, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end

has_secure_passwordを記述することにより、新規登録時passwordpassword_confirmationの2つを入力することで、password_digestとして値が変換されDBに保存されるので、ユーザーデータがセキュアな物になります。


ルーティング設定

  • config/routes.rbにルーティングの設定を行います。
config/routes.rb
Rails.application.routes.draw do

  post '/signup', to: 'registrations#signup'

  post '/login', to: 'sessions#login'
  delete '/logout', to: 'sessions#logout'
  get '/logged_in', to: 'sessions#logged_in?'

end

/signupでは、Registrationsコントローラのcreateアクションへ、POSTリクエストを送信し、新規登録することになります。

/loginでは、SessionsコントローラのcreateアクションへPOSTリクエストを送信し、ログインすることになります。/logoutもこれと同様の動きです。

/logged_inでは、React側でユーザーのログイン状態を追跡するために必要なルーティングです。詳しくはReactでの機能実装時に説明します。


コントローラの作成

  • controllers/registrations_controller.rbを作成し、以下のようにコードを記述。
app/controllers/registrations_controller.rb
class RegistrationsController < ApplicationController

    def signup
        @user = User.new(registrations_params)

        if @user.save
            login!
            render json: { status: :created, user: @user }
        else
            render json: { status: 500 }
        end
    end

    private

        def registrations_params
            params.require(:user).permit(:email, :password, :password_confirmation)
        end

end

ここでは、React側からPOST送信されるユーザーデータをDBに保存し、その後JSONとして返り値をレンダリングしています。

login!は、セッションを使ってユーザーをログインさせるヘルパーメソッドで、後で定義します。

React側では、ここでレンダリングされたJSONデータを取得して扱います。

それから、ここでは新規登録の処理になるので、registrations_paramsのパラメータにてpasswordと共にpassword_confirmationを渡していることに注意してください。

  • controllers/sessions_controller.rbファイルを作成し、以下のようにコードを記述する。
app/controllers/sessions_controllers.rb
class SessionsController < ApplicationController
    def login
        @user = User.find_by(email: session_params[:email])

        if @user && @user.authenticate(session_params[:password])
            login!
            render json: { logged_in: true, user: @user }
        else
            render json: { status: 401, errors: ['認証に失敗しました。', '正しいメールアドレス・パスワードを入力し直すか、新規登録を行ってください。'] }
        end
    end

    def logout
        reset_session
        render json: { status: 200, logged_out: true }
    end

    def logged_in?
        if @current_user
            render json: { logged_in: true, user: current_user }
        else
            render json: { logged_in: false, message: 'ユーザーが存在しません' }
        end
    end

    private

        def session_params
            params.require(:user).permit(:username, :email, :password)
        end

end

loginアクション:ログインの処理を行います。if文の部分では、実際にユーザーが入力したpassword・password_confirmationの値と、DBに保存されているpassword_digestの値を比較し、合致した場合にtrueを返します。

logoutアクション:ログアウトの処理を行います。
  
logged_in?アクション:ユーザーのログイン状態をReactへ返します。@current_userはログイン中のユーザーを表すインスタンス変数で、このログイン中のユーザーが存在していれば、それに応じて適切な値を返します。
  
JSON内の値がどのように使うのかは、React側で機能を実装する際にその都度説明します。

  • controllers/application_controller.rbに、必要事項を記述していく。
app/controllers/application_controller.rb
class ApplicationController < ActionController::Base

    skip_before_action :verify_authenticity_token

    helper_method :login!, :current_user

    def login!
        session[:user_id] = @user.id
    end

    def current_user
        @current_user ||= User.find(session[:user_id]) if session[:user_id]
    end

end

skip_before_action :verify_authenticity_token:Railsが認証トークンを使用しないようにする。認証トークンというのは、CSRF攻撃を防ぐために、Railsフォームからコントローラへと送信されるパラメータの中に追加されるセキュリティートークンのことですが、ここでは使用しないのでスキップさせます。
  
login!:セッションを使ってユーザーをログインさせます。
  
@current_user:ログイン中のユーザーを取得するインスタンス変数を定義しています。


2.React側の実装

React Hooksを使用するため、コンポーネントは関数コンポーネントで構築し、初期値の設定はuseState()フックを使用し、レンダリング後のマウント処理はuseEffect()フックを使用します。

Reactアプリケーションのセットアップ

1.create-react-appを使ってReactアプリケーションを構築する
$ npx create-react-app auth-app-client && cd auth-app-client
2.アプリケーションが構築されたら、不要なファイルを削除する。

  →App.css、App.test.js、logo.svg

3.src/App.jsコンポーネントの状態を、一旦以下のようにすっきりさせる。
src/App.js
import React from 'react';

export default function App() {
  render() {
    return (
      <div>

      </div>
    )
  }
}

  ・デフォルトではクラスコンポーネントで構築されていると思うので、関数コンポーネントに書き換えておきます。
  ・React Hooksは関数コンポーネントでしか使用できないので、クラスコンポーネントで実装してしまわないように注意してください。

4.srx/index.jsファイルの中身も、以下のようにすっきりとさせる。
src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

ReactDOM.render( <App />, document.getElementById('root') );
5.必要な依存パッケージをyarn addコマンドでインストールする。
$ yarn add axios react-router-dom

  ・axios:APIとの非同期通信を行うのに必要なパッケージ。
  ・react-router-dom:Reactアプリ内でのルーティング設定に必要なパッケージ。


react-router-domでルーティングを設定する

それぞれのコンポーネントへと導くルータの役割をApp.jsに担ってもらうことにします。
そのApp.jsに、今から作成するHome.jsコンポーネントDashboard.jsコンポーネントへのルーティングを作成していきます。

1.Home.jsコンポーネントを作成する。
src/components/Home.js
import React from 'react'

export default function Home() {
    return (
        <div>
            <h1>Home</h1>
        </div>
    )
}

  ・src/componentsディレクトリを新たに作成し、そこにHome.jsファイルを作成する。
  ・このHome.jsコンポーネントをルートURL("localhost:3000")とし、Home.jsコンポーネントに認証系機能のフォームなどをやログインフォームなどを作成していきます。

2.Dashboard.jsコンポーネントを作成する。
react/src/components/Dashboard.js
import React from 'react'

export default function Dashboard() {
    return (
        <div>
            <h1>Dashboard</h1>
        </div>
    )
}

  ・src/componentsに、Dashboard.jsファイルを作成します。
  ・Dashboard.jsコンポーネントの役割は、ログイン処理が完了した後にリダイレクトされるページとしてのみ使用します。

3.App.jsに、Home.jsコンポーネントとDashboard.jsコンポーネントへのルーティングを作成する。
src/App.js
import React from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'

import Home from './components/Home'
import Dashboard from './components/Dashboard'

export default function App() {
  return (
    <div>
      <BrowserRouter>
        <Switch>
          <Route exact path={"/"} component={Home} />
          <Route exact path={"/dashboard"} component={Dashboard} />
        </Switch>
      </BrowserRouter>
    </div>
  )
}

  ・import { BrowserRouter, Switch, Route } from 'react-router-dom':react-router-domからルーティングの設定に必要なコンポーネントをimport。ルーターはルートのコンポーネントで使用すること。

  ・次にHome.js、Dashboard.jsコンポーネントをimport。

  ・<Route exact path={"/"} component={Home} />Routeコンポーネントを使ってHomeコンポーネントへのルーティングを設定。pathは"/"。Dashboardコンポーネントへのルーティングpathは"/dashboard"とします。

  ・ルーターは、RouteコンポーネントをSwitchコンポーネントでラッピングし、それらをさらにBrowserRouteコンポーネントでラップすることで動作する。

  ・react-router-dom公式:https://reactrouter.com/web/guides/quick-start

ここまでの状態で$ yarn startコマンドを実行しlocalhost:3000へアクセスしてみると、Home.jsコンポーネントがレンダリングされていることが確認でき、URLに/dashboardを付け加えるとDashboard.jsコンポーネントがレンダリングされることが確認できます。


新規登録コンポーネントを実装する

新規登録コンポーネントでは、ユーザーの新規登録機能の実装と、新規登録と同時にユーザーをログインさせる機能を実装します。

1.Registration.jsコンポーネントを作成する。
src/components/auth/Registrations.js
import React from 'react'

export default function Registration() {
    return (
        <div>
           <p>新規登録</p>
        </div>
    )
}

  ・src/componentsディレクトリ配下に、新たにauthディレクトリを作成し、auth内にRegistration.jsを作成する。
  ・まずはRegistrationコンポーネントが、正常にレンダリングされるかどうかを確認するので、とりあえず記述はこれだけにしておきます。

2.Registration.jsコンポーネントを、Home.jsでレンダリングする。
src/components/Home.js
import React from 'react'
import Registration from './auth/Registration'

export default function Home() {
    return (
        <div>
            <h1>Home</h1>
            <Registration  />
        </div>
    )
}

  ・新規登録フォームはHomeページで表示させたいので、Registrationコンポーネントをレンダリングさせます。
  ・ブラウザで以下のように表示されていれば、正しくレンダリングが動作しています。
image.png

3.Registrationコンポーネントにユーザーオブジェクトの初期値を定義する。
src/components/auth/Registration.js
import React from 'react'
// useStateフックをimportする
import React, { useState } from 'react'

export default function Registration() {
    // useState()を用いて、ユーザーデータの初期値(空の文字列)を定義する。
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")

    return (
        <div>
           <p>新規登録</p>
        </div>
    )
}

  ・useState()フックは、[引数1, 引数2]のように配列で初期値を定義する。引数1には変数を定義し、引数2には関数を定義します。ユーザーデータを参照する場合は引数1の変数の方を使用し、ユーザーデータを書き換えたい場合は引数2の関数の方を使用します。

4.Registrationコンポーネントに新規登録フォームを作成する。
src/components/auth/Registration.js
import React from 'react'
import React, { useState } from 'react'

export default function Registration() {
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")

    return (
        <div>
           <p>新規登録</p>

           {/* 追加 */}
            <form>
                <input
                    type="email"
                    name="email"
                    placeholder="メールアドレス"
                    value={email}
                />
                <input
                    type="password"
                    name="password"
                    placeholder="パスワード"
                    value={password}
                />
                <input
                    type="password"
                    name="password_confirmation"
                    placeholder="確認用パスワード"
                    value={passwordConfirmation}
                />

                <button type="submit">登録</button>
            </form>
        </div>
    )
}

  ・ここでは、先ほどuseState()で定義した初期値を使っています。先ほども述べた通り、データの参照は第1引数の変数で行いますので、valueにはユーザーデータの変数を渡しています。

5.onSubmitイベントと、onChangeイベントの定義。
src/components/auth/Registration.js
import React from 'react'
import React, { useState } from 'react'

export default function Registration() {
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")

    const handleSubmit = (event) => {
        console.log("イベント発火")
        event.preventDefault()
    }

    return (
        <div>
           <p>新規登録</p>

           {/* onSubmit、onChangeイベントを追加 */}
            <form onSubmit={handleSubmit}>
                <input
                    type="email"
                    name="email"
                    placeholder="メールアドレス"
                    value={email}
                    onChange={event => setEmail(event.target.value)}
                />
                <input
                    type="password"
                    name="password"
                    placeholder="パスワード"
                    value={password}
                    onChange={event => setPassword(event.target.value)}
                />
                <input
                    type="password"
                    name="password_confirmation"
                    placeholder="確認用パスワード"
                    value={passwordConfirmation}
                    onChange={event => setPasswordConfirmation(event.target.value)}
                />

                <button type="submit">登録</button>
            </form>
        </div>
    )
}

  
  ・onSubmitには、あらかじめhandleSubmitイベントハンドラを定義し、それを渡しています。
  
  ・handleSubmitは「登録」ボタンがクリックされた時に発火します。ここではイベントハンドラの動作を確認するために、処理を一旦console.log()としていますが、実際にはRails側にデータを送信する処理をここに記述します。event.preventDefault()ではフォームの値を初期化します。クリック後も値が残り続けてしまうので。

  ・onChangeは、フォームを入力している最中に発火し、入力した値でユーザーデータを書き換えます。
   例えば、emailフォームに文字列を入力→setEmail関数によりemail変数の値がその文字列に書き換わる→書き換わったemail変数の値がvalueに渡される。onChangeイベントはこのサイクルを繰り返します。
  ・onChangeでは、関数を定義していません。関数を定義するよりも直接値を処理する記述を行う方が楽なのでこうしてます。
 
  ・ブラウザで、ボタンをクリックしてコンソールにconsole.log()の値が表示されていれば正常の動作しています。

6.次は、axiosを使って、フォームに入力したユーザーデータをRails側に送信する機能を実装します。
src/components/auth/Registration.js
...

// 追加
import axios from 'axios'

export default function Registration() {
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")

    const handleSubmit = (event) => {

        //追加
        axios.post("http://localhost:3001/signup",
            {
                user: {
                    email: email,
                    password: password,
                    password_confirmation: passwordConfirmation
                }
            },
            { withCredentials: true }
        ).then(response => {
            console.log("registration res", response)
        }).catch(error => {
            console.log("registration error", error)
        })
        event.preventDefault()

    }

    return (
        <div>
           <p>新規登録</p>

            <form onSubmit={handleSubmit}>
                <input
                    type="email"
                    name="email"
                    placeholder="メールアドレス"
                    value={email}
                    onChange={event => setEmail(event.target.value)}
                />
                <input
                    type="password"
                    name="password"
                    placeholder="パスワード"
                    value={password}
                    onChange={event => setPassword(event.target.value)}
                />
                <input
                    type="password"
                    name="password_confirmation"
                    placeholder="確認用パスワード"
                    value={passwordConfirmation}
                    onChange={event => setPasswordConfirmation(event.target.value)}
                />

                <button type="submit">登録</button>
            </form>
        </div>
    )
}

  ・axios.post(第1引数, 第2引数, 第3引数):第1引数にはデータを送信したいRails側のURL、第2引数にはユーザーオブジェクト、第3引数にはwithCredentials: trueという値を渡してます。

  ・第1引数のURLには"/signup"を指定していますので、Railsの"/signup"ルーティング→Registrationsコントローラsignupアクションへとデータが渡されて行きます。

  ・Raileへ渡すデータは、上記の通り第2引数のユーザーオブジェクトです。渡しているemailpasswordpassword_confirmationは、useState()フックで定義されている変数のことです。先ほども言った通り、データを参照する場合は変数の方を使用します。

  ・withCredentials: true(必須)の部分では、axiosでバックエンドのAPI(Rails)と通信する際にデータにcookieを含めるかどうかを決める物で、trueにすることで含めることができ、今回の認証機能にはこのcookieを使用するので、trueとしてください

  ・さて、ここで一旦Rails側で定義したコントローラを見てみます。これを見ればわかる通り、React側で送信するユーザーオブジェクトは、registration_paramsへ渡されます。そしてそのデータを基に、ユーザーが新たに作成・保存されます。
スクリーンショット 2020-08-26 20.07.21.png

  ・次に、thenの部分です。これはコントローラがユーザーの作成に成功した場合に、引数でRailsから戻り値(その他様々なデータ)を取得し、その戻り値を使って処理を行います。実際にはここにReact側でユーザーをログインさせる処理を記述していきますが、一旦console.log()を記述し、Railsから値が返ってくるかどうかをテストします。

  ・catch部分では、ユーザーの作成に失敗した場合に、Railsからの戻り値としてエラーを取得します。

  ・Railsからの戻り値がどういうものかは、上記のRegistrationsコントローラの画像で確認できますが。どちらもJSONとしてデータが返って来ます。成功した場合は{status: created, user: @user}という値が、失敗した場合には{status: 500}が返って来ます。

  ・以下の画像は、then部分で記述したconsole.log("registration res", response)の中身です。ユーザーの登録が成功した場合には以下の値が返って来ます。
   特に大事な部分は、data:{ status:..., user: {...} }の部分で、このデータををReact側で扱うことになります。
image.png


新規登録コンポーネントまとめ

 1.Registrationコンポーネントを作成する。
 2.HomeコンポーネントにRegistrationコンポーネントをレンダリングさせる。
 3.Registrationコンポーネントに、useState()フックをimportし、初期値を定義する。
 4.Registrationコンポーネントに新規登録フォームを作成する。
 5.onSubmitイベント、onChangeイベントを定義(仮)する。
 6.axiosを使って、handleSubmitイベントハンドラ内に、ユーザーデータをRails側に送信する機能を実装する。
 ※handleSubmit内にログイン処理を書く前に、ログイン状態をチェックする機能を先に実装します。


ログイン状態をチェックする機能を実装する

1.App.jsコンポーネントに、ログイン状態をチェックするのに必要な初期値を定義する。
src/App.js
// 追加する
import React, { useState } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'

import Home from './components/Home'
import Dashboard from './components/Dashboard'

export default function App() {
  // 追加する
  const [loggedInStatus, setLoggedInStatus] = useState("未ログイン")
  const [user, setUser] = useState({})

  return (
    <div>
      <BrowserRouter>
        <Switch>
          <Route exact path={"/"} component={Home} />
          <Route exact path={"/dashboard"} component={Dashboard} />
        </Switch>
      </BrowserRouter>
    </div>
  )
}

  ・ログイン状態のチェックは、複数のコンポーネントで使用したいので、全てのコンポーネントのであるApp.jsに定義し、他のどのコンポーネントでも共有できるようにします。

  ・初期値の定義なのでまずはuseStateフックをimportします。

  ・loggedInStatusオブジェクトuserオブジェクトの初期値を定義しました。

  ・loggedInStatus変数では、ユーザーのログイン状態を参照します。デフォルトではログインしていない状態を表す"未ログイン"としています。

  ・userオブジェクトは、実際にユーザーをログインさせる際に必要になります。

2.Render Propsという手法を用いて、App.js(親)からHome.js(子)へ必要な値をpropsとして渡す。
src/App.js
...

export default function App() {
  const [loggedInStatus, setLoggedInStatus] = useState("未ログイン")
  const [user, setUser] = useState({})

  return (

    <BrowserRouter>
      ...

          {/* 追加する */}
          <Route
            exact path={"/"}
            render={props => (
              <Home { ...props } loggedInStatus={loggedInStatus} />
            )}
          />

      ...
    <BrowserRouter />

  )
}

  ・Render Propsを用いるとそのコンポーネント記述したコードを、別のコンポーネントに引き継ぐことができます。

  ・render={ props => ( <コンポーネント { ...props } /> ) }とすることで、当該コンポーネントで定義している内容を、指定したコンポーネントへpropsとして渡すことができます。

  ・ちなみに、ここで渡しているpropsの中身を確認してみると、以下のようになっています。
image.png

  ・{ ...props }という書き方については、https://stackoverflow.com/questions/28452358/what-is-the-meaning-of-this-props-in-reactjs で分かり易く解説してくれています。

  ・そしてここにはloggedInStatus={loggedInStatus}という記述がありますが、これはHome.js内のloggedInStatusという変数に、App.jsのloggedInStatus変数を代入していることを表します。この変数をHome.jsにてレンダリングしてあげることで、Home.jsでログイン状態を表示させることが可能になります。
   
  (・正直ここに関してはHooksなりの良い書き方があるとは思うのですが、Hooks初心者なので今回はこれで妥協しました…。)

3.Home.jsで、ユーザーのログイン状態を表示する。
src/components/Home.js
import React from 'react'
import Registration from './auth/Registration'

// propsを引数に書き加える
export default function Home(props) {

    return (
        <div>
            <h1>Home</h1>

            {/* 追加する */}
            <h2>ログイン状態: {props.loggedInStatus}</h2>
            <Registration  />
        </div>
    )
}

  ・関数コンポーネント自体に引数としてpropsを渡します、渡し忘れに注意。
  ・取得したpropsから、先ほどApp.jsで渡したloggedInStatus変数を取り出して表示させます。
  ・ブラウザで正常に表示されていることを確認します。
image.png

4.Dashboard.jsにも同様に、Render Propsを用いてログイン状態を表示させる。
src/App.js
return (

    <BrowserRouter>
      ...

          {/* 追加する */}
          <Route
            exact path={"/"}
            render={props => (
              <Dashboard { ...props } loggedInStatus={loggedInStatus} />
            )}
          />

      ...
    <BrowserRouter />

  )
src/components/Dashboard.js
import React from 'react'

export default function Dashboard(props) {
    return (
        <div>
            <h1>Dashboard</h1>
            <h2>ログイン状態: {props.loggedInStatus}</h2>
        </div>
    )
}

image.png

ログイン状態をチェックする機能まとめ

 1.App.jsコンポーネントに、ログイン状態をチェックするのに必要な初期値を定義する。
 2.Render Propsという手法を用いて、App.js(親)からHome.js(子)へ必要な値をpropsとして渡す。
 3.Home.jsで、ユーザーのログイン状態を表示する。
 4.Dashboard.jsにも同様に、Render Propsを用いてログイン状態を表示させる。


ログイン機能を実装する

ログイン状態を確認できる機能が実装できたので、次は実際にユーザーがログインできる機能を実装します。
※Registration.jsコンポーネントのhandleSubmitイベントの機能が途中で終わっているので、その続きとしてログイン機能を実装します。

1.Registration.jsのhandleSubmitイベント内のthen()の中に、ユーザーをログインさせるための記述を行う(仮)
src/components/auth/Registration.js
import React, { useState } from 'react'
import axios from 'axios'

// 引数にpropsを渡す
export default function Registration(props) {

    ...

    const handleSubmit = (event) => {
        axios.post("http://localhost:3001/signup",
            {
                user: {
                    email: email,
                    password: password,
                    password_confirmation: passwordConfirmation
                }
            },
            { withCredentials: true }
        ).then(response => {

            // 追加
            if (response.data.status === 'created') {
                props.handleSuccessfulAuthentication(response.data)
            }

        }).catch(error => {
            console.log("registration error", error)
        })


        event.preventDefault()
    }

    return (
       ...
    )
}

  ・if文の部分:以下の画像は先ほど見たのと同様、新規登録ボタンを押した際にRailsから返ってくるデータだが、ユーザーの登録に成功すると、response.data.status"created"というステータスが返ってくる(そうRails側で設定したので)。なので、ステータスが"created"となったら正しいイベントが発火するように条件式を定義します。
image.png

  ・条件式内の処理では、propsからhandleSuccessfulAuthentication()イベントハンドラを取得し、そこにresponseで受け取ったデータのdataフィールドを渡しています。

  ・見たらわかる通りイベントハンドラはpropsから取り出していますので、イベントハンドラは別のコンポーネントで定義し、それをRegistrationコンポーネントへ渡していることがわかります。

  ・このイベントハンドラこそまさに、ユーザーをログインさせる処理を実行するイベントハンドラになります。これは今記述を行っている新規登録フォームでも使いますが、この後作成するログインフォームでも使用することになります。つまり2度使うということなので、親コンポーネントにて関数化してそれを再利用しようということです。

2.Home.jsに、handleSuccessfulAuthentication()イベントハンドラを定義する。
src/components/Home.js
import React from 'react'
import Registration from './auth/Registration'

export default function Home(props) {

    // 追加
    const handleSuccessfulAuthentication = (data) => {
        props.history.push("/dashboard")
    }

    return (
        <div>
            <h1>Home</h1>
            <h2>ログイン状態: {props.loggedInStatus}</h2>

            {/* 書き加え */}
            <Registration handleSuccessfulAuthentication={handleSuccessfulAuthentication}
 />
        </div>
    )
}

  ・handleSuccessfulAuthenticationでは、引数を渡してますが、これはユーザーオブジェクトのことです。処理としてprops.history.push("/dashboard")となっていますが、これは"/dashboard"へ画面遷移させる処理です。手始めに画面遷移するかどうかをチェックし、このイベントハンドラが正常に動作するかどうかをテストします。

  ・「ログイン状態をチェックするの項目2」で、Homeに渡されるpropsの画像を上の方で見せましたが、その中に"history"というフィールドがあり、"/"URLでした。そこに"/dashboard"pushすることで画面遷移を実現しています。

  ・「登録」ボタンを押してみて、以下のように遷移すればイベントハンドラは正常に動作しています。
image.png
image.png

3.handleLogin()イベントハンドラを、handleSuccessfulAuthenticationイベントハンドラ内に記述する。
src/components/Home.js
const handleSuccessfulAuthentication = (data) => {
        props.handleLogin(data)
        props.history.push("/dashboard")
}

  ・見てわかる通りhandleLogin()イベントハンドラは、propsから取り出しています。ということは親コンポーネントにて関数化して、それをここで再利用するということです。handleLogin()イベントハンドラはこの後App.jsにて定義します。

4.App.jsに、handleLoginイベントハンドラを定義する。
src/App.js
export default function App() {
  const [loggedInStatus, setLoggedInStatus] = useState("未ログイン")
  const [user, setUser] = useState({})

  // 追加する
  const handleLogin = (data) => {
    setLoggedInStatus("ログインなう")
    setUser(data.user)
  }

  return (
     ...

          <Route
            exact
            path={"/"}
            render={props => (
              // 追加する
              <Home { ...props } handleLogin={handleLogin} loggedInStatus={loggedInStatus} />
            )}
          />

     ...
  )
}

  ・handleLoginイベントハンドラでは、setLoggedInStatus()関数を用いて、loggedInStatusオブジェクトの値を書き換えます。次に、引数で取得したdataからuserフィールドを取得し、setUser()関数に渡すことで、userオブジェクトの値を書き換えています。

  ・HomeコンポーネントでhandleLoginイベントハンドラが使用できるように、propsとしてHomeコンポーネントにhandleLoginイベントハンドラを渡しています。


ログイン機能を実装するまとめ

 1.Registration.jsのhandleSubmitイベント内のthen()の中に、ユーザーをログインさせるための記述を行う(仮)
 2.Home.jsに、handleSuccessfulAuthentication()イベントハンドラを定義する。
 3.handleLogin()イベントハンドラを、handleSuccessfulAuthenticationイベントハンドラ内に記述する。
 4.App.jsに、handleLoginイベントハンドラを定義する。


ここまで実装できたことで、React側でユーザーのログイン状態にすることができるようになりました。ブラウザでユーザーデータを入力し、新規登録を行ってみて、以下のように画面遷移に加え、ログイン状態が変更されれば正常に動作しています。
image.png

image.png

しかし、cookieを保持していないため、ブラウザを更新するとログイン状態はすぐにデフォルトの状態へ戻ってしまいます。なので、後でライフサイクルフックを用いてログイン状態を維持する機能を実装します。
ただその前に、ログインフォームがまだ存在しないので、そちらを先に作成していきます。


ログインコンポーネントを実装する

1.src/components/authディレクトリに、新たにLogin.jsを作成し、まずは、Registration.jsコンポーネントのコードをそのままコピペする。
src/components/auth/Login.js
import React, { useState } from 'react'
import axios from 'axios'

export default function Registration(props) {
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")
    const [passwordConfirmation, setPasswordConfirmation] = useState("")

    const handleSubmit = (event) => {
        axios.post("http://localhost:3001/signup",
            {
                user: {
                    email: email,
                    password: password,
                    password_confirmation: passwordConfirmation
                }
            },
            { withCredentials: true }
        ).then(response => {
            if (response.data.status === 'created') {
                props.handleSuccessfulAuthentication(response.data)
            }
        }).catch(error => {
            console.log("registration error", error)
        })
        event.preventDefault()
    }

    return (
        <div>
            <p>新規登録</p>

            <form onSubmit={handleSubmit}>
                <input
                    type="email"
                    name="email"
                    placeholder="メールアドレス"
                    value={email}
                    onChange={event => setEmail(event.target.value)}
                />
                <input
                    type="password"
                    name="password"
                    placeholder="パスワード"
                    value={password}
                    onChange={event => setPassword(event.target.value)}
                />
                <input
                    type="password"
                    name="password_confirmation"
                    placeholder="確認用パスワード"
                    value={passwordConfirmation}
                    onChange={event => setPasswordConfirmation(event.target.value)}
                />

                <button type="submit">登録</button>
            </form>
        </div>
    )
}
2.Registration→Login関数コンポーネントになるよう書き換え、password_confirmationフィールドは使わないので削除し、axios通信する先のURLを/loginに変更する。
src/components/auth/Login.js
import React, { useState } from 'react'
import axios from 'axios'

// Login関数コンポーネントへ書き換え
export default function Login(props) {
    // password_confirmationフィールドを削除
    const [email, setEmail] = useState("")
    const [password, setPassword] = useState("")

    const handleSubmit = (event) => {
        // 通信先のURLを/loginに書き換え
        axios.post("http://localhost:3001/login",
            {
                // ここのpassword_confirmationフィールドも削除
                user: {
                    email: email,
                    password: password,
                }
            },
            { withCredentials: true }
        ).then(response => {
            if (response.data.status === 'created') {
                props.handleSuccessfulAuthentication(response.data)
            }
        }).catch(error => {
            console.log("registration error", error)
        })
        event.preventDefault()
    }

    return (
        <div>
            {/* ログインに変更 */}
            <p>ログイン</p>

            {/* フォーム内のpassword_confrmation入力フィールド削除 */}
            <form onSubmit={handleSubmit}>
                <input
                    type="email"
                    name="email"
                    placeholder="メールアドレス"
                    value={email}
                    onChange={event => setEmail(event.target.value)}
                />
                <input
                    type="password"
                    name="password"
                    placeholder="パスワード"
                    value={password}
                    onChange={event => setPassword(event.target.value)}
                />

                <button type="submit">ログイン</button>
            </form>
        </div>
    )
}
3.次に、/loginへPOSTリクエストを送信した際に、正しく返り値を取得できているかどうかを検証する。
src/components/auth/Login.js

...

const handleSubmit = (event) => {
        axios.post("http://localhost:3001/login",
            {
                user: {
                    email: email,
                    password: password,
                }
            },
            { withCredentials: true }
        ).then(response => {
            // 追加
            console.log("login response: ", response)
            // if (response.data.status === 'created') {
            //    props.handleSuccessfulAuthentication(response.data)
            // }
        }).catch(error => {
            console.log("registration error", error)
        })
        event.preventDefault()
    }

...

  ・axios通信のthen()内の記述をconsole.log()処理に一旦変更します。

4.Home.jsLoginコンポーネントを表示させる。
src/components/Home.js
...
// 追加
import Login from './auth/Login'

export default function Home(props) {

    const handleSuccessfulAuthentication = (data) => {
        props.handleLogin(data)
        props.history.push("/dashboard")
    }

    return (
        <div>

            ...
            {/* 追加 */}
            <Login />
        </div>
    )
}
5.ブラウザで動作確認してみる。

  ・ユーザーデータを入力し、ログインボタンをクリック。

image.png

  ・コンソールで返り値を見てみます。

image.png

  ・入力したユーザーのデータがRails側で正常にログイン実行がされた結果、正しいユーザーデータが返ってきているので、ログインコンポーネントの方も正常に動作していることがわかります。

  ・それと、dataフィールドに{ logged_in: true }というデータがあることに注目してください。これは、/loginへアクセスした場合にのみ返ってくるデータです。これはこの後axios通信のthen()の処理にて使用します。

6.Loginコンポーネントで定義されているhandleSubmitイベントハンドラのaxios通信におけるthen()の条件式を変更する。
src/components/auth/Login.js
const handleSubmit = (event) => {
        axios.post("http://localhost:3001/login",
            {
                user: {
                    email: email,
                    password: password,
                }
            },
            { withCredentials: true }
        ).then(response => {

            // 変更
            if (response.data.logged_in) {
                props.handleSuccessfulAuthentication(response.data)
            }

        }).catch(error => {
            console.log("registration error", error)
        })
        event.preventDefault()
    }

  ・先ほども見たように、ログインした場合にはRailsからlogged_inフィールドが返ってくるので、これを条件式に設定し、trueの場合にログイン処理が実行されるようにします。新規登録の際に行われるログイン処理と差別化するためです。

7.Home.jsコンポーネントでレンダリングしているLoginコンポーネントに、handleSuccessfulAuthenticationイベントハンドラを渡す。
src/components/Home.js
export default function Home(props) {

    const handleSuccessfulAuthentication = (data) => {
        props.handleLogin(data)
        props.history.push("/dashboard")
    }

    return (
        <div>
            <h1>Home</h1>
            <h2>ログイン状態: {props.loggedInStatus}</h2>
            <Registration handleSuccessfulAuthentication={handleSuccessfulAuthentication} />

            {/* 書き加え */}
            <Login handleSuccessfulAuthentication={handleSuccessfulAuthentication} />
        </div>
    )
}


ログインコンポーネントを実装するまとめ

 1.src/components/authディレクトリに、新たにLogin.jsを作成し、まずは、Registration.jsコンポーネントのコードをそのままコピペする。
 2.Registration→Login関数コンポーネントになるよう書き換え、password_confirmationフィールドは使わないので削除し、axios通信する先のURLを/loginに変更する。
 3.次に、/loginへPOSTリクエストを送信した際に、正しく返り値を取得できているかどうかを検証する。
 4.Home.jsLoginコンポーネントを表示させる。
 5.ブラウザで動作確認してみる。
 6.Loginコンポーネントで定義されているhandleSubmitイベントハンドラのaxios通信におけるthen()の条件式を変更する。
 7.Home.jsコンポーネントでレンダリングしているLoginコンポーネントに、handleSuccessfulAuthenticationイベントハンドラを渡す。


ログイン状態を維持する機能を実装する。

ここでは、useEffect()というライフサイクルフックを使い、ページレンダリング後に毎回、ユーザーのログイン状態を追跡するようにします。これによってログイン状態の維持を実現させます。

1.App.jsコンポーネントに、ユーザーのログイン状態がチェックできるライフサイクルを定義する。
src/App.js
// useEffectフックを追加
import React, { useState, useEffect } from 'react'
// axiosをimport
import axios from 'axios'
...

import Home from './components/Home'
import Dashboard from './components/Dashboard'

export default function App() {
  const [loggedInStatus, setLoggedInStatus] = useState("未ログイン")
  const [user, setUser] = useState({})

  const handleLogin = (data) => {
    setLoggedInStatus("ログインなう")
    setUser(data.user)
  }

  // 追加
  useEffect(() => {
    checkLoginStatus()
  })

  // 追加
  const checkLoginStatus = () => {
    axios.get("http://localhost:3001/logged_in", { withCredentials: true })
      .then(response => {
      console.log("ログイン状況", response)
    }).catch(error => {
      console.log("ログインエラー", error)
    })
  }

  return (
    ...
  )
}

  ・checkLoginStatus関数:ここでは、Railsの"/logged_in"へ、withCredentials: trueというデータを含め、GETリクエストを送信しています。

  ・useEffect()フックは、ページがリロードされるたびに毎回呼び出されます。useEffect()にcheckLoginStatus()関数を渡すことで、リロード後毎回この関数を呼び出すようにします。

  ・例えば、/logged_inへGETリクエストを送信すると、以下で示しているSessionsコントローラのlogged_in?アクションへデータが送信されます。ここでは@current_userの存在をチェックし、trueなら{logged_in: true, user: @current_user}というJSONオブジェクト、falseなら{logged_in: false}を返します。
スクリーンショット 2020-08-27 16.02.36.png
  ・Railsからの返り値をconsole.log()で示したのが以下の画像です。つまり、ページがリロードされるたびに、この返り値が返ってくるということになります。
image.png

2.checkLoginStatusthen()内で返り値に対する処理を記述する。
src/App.js
const checkLoginStatus = () => {
    axios.get("http://localhost:3001/logged_in", { withCredentials: true })

      .then(response => {
        if (response.data.logged_in && loggedInStatus === "未ログイン") {
          setLoggedInStatus("ログインなう")
          setUser(response.data.user)
        } else if (!response.data.logged_in && loggedInStatus === "ログインなう") {
          setLoggedInStatus("未ログイン")
          setUser({})
        }
      })

      .catch(error => {
        console.log("ログインエラー", error)
    })
}

  ・if (response.data.logged_in && loggedInStatus === "未ログイン")の部分:返り値に含まれるdataフィールドのlogged_inフィールドがtrueなのに、React側のloggedInStatus"未ログイン"となっている場合に、ログイン状態を"ログインなう"に書き換えし、userオブジェクトも返り値から受け取ったユーザーオブジェクトに書き換えます

  ・else if (!response.data.logged_in && loggedInStatus === "ログインなう")の部分:logged_inフィールドがfalseなのに、loggedInStatusが"ログインなう"になってしまっている場合に、loggedInStatusオブジェクトを空にし、userオブジェクトも空にします。


ログイン状態を維持する機能を実装するまとめ

 1.App.jsコンポーネントに、ユーザーのログイン状態がチェックできるライフサイクルを定義する。
 2.checkLoginStatusthen()内で返り値に対する処理を記述する。


ログアウト機能を実装する

1.App.jsコンポーネントにhandleLogoutイベントハンドラを定義する。
src/App.js
...

export default function App() {
  const [loggedInStatus, setLoggedInStatus] = useState("未ログイン")
  const [user, setUser] = useState({})

  ...

  // 追加
  const handleLogout = () => {
    setLoggedInStatus("未ログイン")
    setUser({})
  }

  ...

  return (

         ...

              <Home
                {...props}
                handleLogin={handleLogin}
                // 追加する
                handleLogout={handleLogout}
                loggedInStatus={loggedInStatus}
              />
            )}
          />

         ...
  )
}

  ・handleLogoutイベントハンドラが呼び出されることによって、ログイン状態ユーザーオブジェクト共に空になるようにします。

2.Home.jsに、ログアウトボタンと、handleClickイベントハンドラを定義する。
src/components/Home.js
import React from 'react'
import Registration from './auth/Registration'
import Login from './auth/Login'

export default function Home(props) {

    const handleSuccessfulAuthentication = (data) => {
        props.handleLogin(data)
        props.history.push("/dashboard")
    }

    // handleLogoutClickイベントハンドラ
    const handleLogoutClick = () => {
        axios.delete("http://localhost:3001/logout", { withCredentials: true })
            .then(response => {
                props.handleLogout()
            }).catch(error => console.log("ログアウトエラー", error))
    }

    return (
        <div>
            <h1>Home</h1>
            <p>ログイン状態: {props.loggedInStatus}</p>

            {/* ボタン追加 */}
            <button onClick={handleLogoutClick}>ログアウト</button>

            <Registration handleSuccessfulAuthentication={handleSuccessfulAuthentication} />
            <Login handleSuccessfulAuthentication={handleSuccessfulAuthentication} />
        </div>
    )
}

  ・ログアウトボタンをクリック→handleLogoutClickイベントハンドラ発火→RailsへDELETEリクエスト送信→Railsから返り値を取得→propsからhandleLogout()イベントハンドラを呼び出し→ログアウト完了という流れで処理されます。

最後に

実装は以上で完了です!
React初学者なため、色々とルールに反している部分もあると思うので気付き次第改稿していきます!
ご指摘なども遠慮せずして頂けると、嬉しいです。よろしくお願いします🙇‍♂️

51
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
kurawo___D
文系大生│backend developer見習い│Ruby, React, Next.js, TypeScript

Comments

No comments
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account Login
51
Help us understand the problem. What is going on with this article?