はじめに
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を追加し、インストールする。
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
ファイルを作成し、以下のようにコードを記述します。
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を処理できるように設定を行う。
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に変更する。
- 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の設定をします。
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を記述することにより、新規登録時
password
とpassword_confirmation
の2つを入力することで、password_digest
として値が変換されDBに保存されるので、ユーザーデータがセキュアな物になります。
ルーティング設定
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
を作成し、以下のようにコードを記述。
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
ファイルを作成し、以下のようにコードを記述する。
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
に、必要事項を記述していく。
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
コンポーネントの状態を、一旦以下のようにすっきりさせる。
import React from 'react';
export default function App() {
render() {
return (
<div>
</div>
)
}
}
・デフォルトではクラスコンポーネントで構築されていると思うので、関数コンポーネント
に書き換えておきます。
・React Hooks
は関数コンポーネントでしか使用できないので、クラスコンポーネントで実装してしまわないように注意してください。
4.srx/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
コンポーネントを作成する。
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
コンポーネントを作成する。
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コンポーネントへのルーティングを作成する。
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
コンポーネントを作成する。
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
でレンダリングする。
import React from 'react'
import Registration from './auth/Registration'
export default function Home() {
return (
<div>
<h1>Home</h1>
<Registration />
</div>
)
}
・新規登録フォームはHomeページで表示させたいので、Registrationコンポーネントをレンダリングさせます。
・ブラウザで以下のように表示されていれば、正しくレンダリングが動作しています。
3.Registrationコンポーネントにユーザーオブジェクトの初期値
を定義する。
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コンポーネントに新規登録フォーム
を作成する。
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
イベントの定義。
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側に送信
する機能を実装します。
...
// 追加
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引数のユーザーオブジェクト
です。渡しているemail
、password
、password_confirmation
は、useState()フック
で定義されている変数のことです。先ほども言った通り、データを参照する場合は変数の方を使用します。
・withCredentials: true(必須)
の部分では、axios
でバックエンドのAPI(Rails)と通信する際にデータにcookie
を含めるかどうかを決める物で、true
にすることで含めることができ、今回の認証機能にはこのcookieを使用するので、trueとしてください
・さて、ここで一旦Rails側で定義したコントローラ
を見てみます。これを見ればわかる通り、React側で送信するユーザーオブジェクトは、registration_paramsへ渡されます
。そしてそのデータを基に、ユーザーが新たに作成・保存
されます。
・次に、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側で扱うことになります。
新規登録コンポーネントまとめ
1.Registration
コンポーネントを作成する。
2.HomeコンポーネントにRegistrationコンポーネントをレンダリングさせる。
3.Registrationコンポーネントに、useState()
フックをimportし、初期値を定義する。
4.Registrationコンポーネントに新規登録フォーム
を作成する。
5.onSubmit
イベント、onChange
イベントを定義(仮)する。
6.axios
を使って、handleSubmitイベントハンドラ
内に、ユーザーデータをRails側に送信する機能を実装する。
※handleSubmit内にログイン処理を書く前に、ログイン状態をチェックする機能を先に実装します。
ログイン状態をチェックする機能を実装する
1.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として渡す。
...
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の中身を確認してみると、以下のようになっています。
・{ ...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
で、ユーザーのログイン状態を表示する。
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変数を取り出して表示
させます。
・ブラウザで正常に表示されていることを確認します。
4.Dashboard.js
にも同様に、Render Propsを用いてログイン状態を表示させる。
return (
<BrowserRouter>
...
{/* 追加する */}
<Route
exact path={"/"}
render={props => (
<Dashboard { ...props } loggedInStatus={loggedInStatus} />
)}
/>
...
<BrowserRouter />
)
import React from 'react'
export default function Dashboard(props) {
return (
<div>
<h1>Dashboard</h1>
<h2>ログイン状態: {props.loggedInStatus}</h2>
</div>
)
}
ログイン状態をチェックする機能まとめ
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()の中に、ユーザーをログインさせるための記述を行う(仮)
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"となったら正しいイベントが発火するように条件式を定義します。
・条件式内の処理では、propsからhandleSuccessfulAuthentication()イベントハンドラを取得
し、そこにresponseで受け取ったデータのdataフィールド
を渡しています。
・見たらわかる通りイベントハンドラはpropsから取り出していますので、イベントハンドラは別のコンポーネントで定義
し、それをRegistrationコンポーネントへ渡している
ことがわかります。
・このイベントハンドラこそまさに、ユーザーをログインさせる処理を実行するイベントハンドラになります。これは今記述を行っている新規登録フォームでも使いますが、この後作成するログインフォームでも使用することになります。つまり2度使うということなので、親コンポーネントにて関数化してそれを再利用しようということです。
2.Home.js
に、handleSuccessfulAuthentication()イベントハンドラ
を定義する。
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
することで画面遷移を実現しています。
・「登録」ボタンを押してみて、以下のように遷移すればイベントハンドラは正常に動作しています。
3.handleLogin()イベントハンドラ
を、handleSuccessfulAuthenticationイベントハンドラ内に記述する。
const handleSuccessfulAuthentication = (data) => {
props.handleLogin(data)
props.history.push("/dashboard")
}
・見てわかる通りhandleLogin()
イベントハンドラは、props
から取り出しています。ということは親コンポーネントにて関数化して、それをここで再利用するということです。handleLogin()イベントハンドラはこの後App.jsにて定義します。
4.App.js
に、handleLogin
イベントハンドラを定義する。
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側でユーザーのログイン状態にすることができるようになりました。ブラウザでユーザーデータを入力し、新規登録を行ってみて、以下のように画面遷移
に加え、ログイン状態が変更
されれば正常に動作しています。
しかし、cookie
を保持していないため、ブラウザを更新するとログイン状態はすぐにデフォルトの状態へ戻ってしまいます。なので、後でライフサイクルフック
を用いてログイン状態を維持する機能を実装します。
ただその前に、ログインフォームがまだ存在しないので、そちらを先に作成していきます。
ログインコンポーネントを実装する
1.src/components/auth
ディレクトリに、新たにLogin.js
を作成し、まずは、Registration.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
に変更する。
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リクエストを送信した際に、正しく返り値を取得できているかどうかを検証する。
...
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.js
にLogin
コンポーネントを表示させる。
...
// 追加
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.ブラウザで動作確認してみる。
・ユーザーデータを入力し、ログインボタンをクリック。
・コンソールで返り値を見てみます。
・入力したユーザーのデータがRails側で正常にログイン実行がされた結果、正しいユーザーデータが返ってきているので、ログインコンポーネントの方も正常に動作していることがわかります。
・それと、dataフィールドに{ logged_in: true }というデータ
があることに注目してください。これは、/login
へアクセスした場合にのみ返ってくるデータです。これはこの後axios通信のthen()
の処理にて使用します。
6.Login
コンポーネントで定義されているhandleSubmit
イベントハンドラのaxios
通信におけるthen()の条件式
を変更する。
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
イベントハンドラを渡す。
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.js
にLogin
コンポーネントを表示させる。
5.ブラウザで動作確認してみる。
6.Login
コンポーネントで定義されているhandleSubmit
イベントハンドラのaxios
通信におけるthen()の条件式
を変更する。
7.Home.jsコンポーネントでレンダリングしているLoginコンポーネント
に、handleSuccessfulAuthentication
イベントハンドラを渡す。
ログイン状態を維持する機能を実装する。
ここでは、useEffect()
というライフサイクルフックを使い、ページレンダリング後に毎回、ユーザーのログイン状態を追跡
するようにします。これによってログイン状態の維持を実現させます。
1.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}
を返します。
・Railsからの返り値をconsole.log()で示したのが以下の画像です。つまり、ページがリロードされるたびに、この返り値が返ってくる
ということになります。
2.checkLoginStatus
のthen()
内で返り値に対する処理を記述する。
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.checkLoginStatus
のthen()
内で返り値に対する処理を記述する。
ログアウト機能を実装する
1.App.js
コンポーネントにhandleLogout
イベントハンドラを定義する。
...
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
イベントハンドラを定義する。
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初学者なため、色々とルールに反している部分もあると思うので気付き次第改稿していきます!
ご指摘なども遠慮せずして頂けると、嬉しいです。よろしくお願いします🙇♂️