masalibの日記

システム開発、運用と猫の写真ブログです

React Firebase入門 ログイン認証とログアウト処理

ログインができたのでログイン認証とログアウト処理を作りたいと思います。

React Firebase入門シリーズ
1-1・ React Firebase入門 初期設定とsignup - masalibの日記
1-2・ 「react-hook-form」を入れてみた - masalibの日記
1-3・ React Firebase入門 signupの通信エラー対応 - masalibの日記
2・ https://masalib.hatenablog.com/entry/2020/11/27/000000
3・React Firebase入門 ログイン認証とログアウト処理 今ここ

ログイン認証

ログインするとダッシュボード(/dashboard)にリダイレクトするのですが ログイン認証をおこなっていないため、ログインしていない人も/dashboardにアクセスできます。 今後、認証のページを作成するためには

ログイン認証

が必要なのでそちらを作ります。

ログイン認証用のルーターの作成の概要

react-router-domというライブラリーを使ってルーティングをおこなっているのですがそのライブラリーには認証(IF文)などができません。

分岐させるルーターを作るというかオーバーライドさせて分岐させます

ソース

import React from "react"
import { Route, Redirect } from "react-router-dom"
import { useAuth } from "../contexts/AuthContext"

export default function AuthFirebaseRoute({ component: Component, ...rest }) {
  const { currentUser } = useAuth()

  return (
    <Route
      {...rest}
      render={props => {
        return currentUser ? <Component {...props} /> : <Redirect to="/login" />
      }}
    ></Route>
  )
}

このソースは理解できていません

私もこの部分を完全に理解していません。 サンプルソースで記載されているのです。 お決まりという形でお願いします

React Router: Declarative Routing for React.js

function PrivateRoute({ children, ...rest }) {
  let auth = useAuth();
  return (
    <Route
      {...rest}
      render={({ location }) =>
        auth.user ? (
          children
        ) : (
          <Redirect
            to={{
              pathname: "/login",
              state: { from: location }
            }}
          />
        )
      }
    />
  );
}

「 { children, ...rest }」部分ですがpropsで渡ってきた部分のchildrenとそのほかの値に分解している。childrenを出力???WHAT?WHY?

さらに< Route >というタグの中にRedirectのコンポーネントを起動している??WHY?・・・

書いていて悲しいのですが本当に理解できていません・・・😢

stackoverflow.com

const myObj = {
  name: 'John Doe',
  age: 35,
  sex: 'M',
  dob: new Date(1990, 1, 1)
};
const { name: Username, ...rest } = myObj
console.log(Username);
// => John Doe
console.log(rest);
// => { age: 35, sex: 'M', dob: Mon Jan 01 1990 00:00:00 GMT-0800 (PST) }

これだけも理解できて嬉しいです

兎にも角にもにもcurrentUserがある場合は 渡ってきた内容(props)をそのまま出力する。

ない場合は、ログインがページにリダレクトさせる。

三項演算子が嫌いな人は普通のIFでも動きます。

if (currentUser){
    return <Component {...props} />
} else {
   return <Redirect to="/login" />
}
//return currentUser ? <Component {...props} /> : <Redirect to="/login" />

ログアウト処理

signupやLoginと同様にcontextに処理をつくってその関数を共有させるだけです。全文はたぶん見なくても大丈夫かと思う。

クリックすると展開されます(長文なので注意)

import React, { useContext, useState, useEffect } from "react"
import { auth } from "../firebase"

const AuthContext = React.createContext()

export function useAuth() {
    return useContext(AuthContext)
}

export function AuthProvider({ children }) {
    const [currentUser, setCurrentUser] = useState()
    const [loading, setLoading] = useState(true)

    function signup(email, password) {
        return auth.createUserWithEmailAndPassword(email, password)
    }

    function login(email, password) {
      return auth.signInWithEmailAndPassword(email, password)
    }

    function logout() {
      return auth.signOut()
    }

    const value = {
        currentUser,
        signup,
        login,
        logout
    }

    useEffect(() => {
        // Firebase Authのメソッド。ログイン状態が変化すると呼び出される
        auth.onAuthStateChanged(user => {
          setCurrentUser(user);
          setLoading(false)
        });
      }, []);

    return (
        <AuthContext.Provider value={value}>
           {!loading && children}
        </AuthContext.Provider>
    )
}    

追加したのはlooutという関数

function logout() {
   return auth.signOut()
}

共通するためのvalueの値を修正

const value = {
    currentUser,
    signup,
    login,
+   logout
}

ダッシュボードにログアウト処理をつける

全文はたぶん見なくても大丈夫かと思う。

クリックすると展開されます(長文なので注意)

import React, { useState }  from 'react'
import { useAuth } from "../contexts/AuthContext"
import { Link, useHistory } from "react-router-dom"
import Button from "@material-ui/core/Button";
import { createStyles, makeStyles, Theme } from "@material-ui/core/styles";

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    container: {
      display: "flex",
      flexWrap: "wrap",
      width: 400,
      margin: `${theme.spacing(0)} auto`
    },
    looutBtn: {
      marginTop: theme.spacing(2),
      flexGrow: 1
    },
    header: {
      textAlign: "center",
      background: "#212121",
      color: "#fff"
    },
    card: {
      marginTop: theme.spacing(10)
    }
  })
);


const Dashboard = () => {
    const classes = useStyles();
    const [error, setError] = useState("")
    const { currentUser, logout } = useAuth()
    const history = useHistory()
  
    async function handleLogout() {
      setError("")
  
      try {
        await logout()
        history.push("/")
      } catch {
        setError("Failed to log out")
      }
    }
    return (
        <div>
            Dashboard
            テスト用のリンク(あとで治す)
            {error && {error}}
            <strong>Email:</strong> {currentUser.email}
            <h2>
                <Link to="/login">Login</Link>
            </h2>
            <h2>
                <Link to="/signup">signup</Link>
            </h2>
            <div>
                <Button
                variant="contained"
                size="large"
                color="secondary"
                className={classes.loginBtn}
                onClick={handleLogout}
            >
                    Logout
                </Button>

            </div>
        </div>
    )
}

export default Dashboard

ポイントとしてはログアウトのボタンが押されたら

    async function handleLogout() {
      setError("")
  
      try {
        await logout()
        history.push("/")
      } catch {
        setError("Failed to log out")
      }
    }

が呼ばれて、contextで共有したlogout処理が実行される。 通信のエラーハンドリングはしていないがあとで入れます。

結果

ログアウト前

f:id:masalib:20201126031035j:plain
ログアウト前

ログアウト後

f:id:masalib:20201126031053j:plain
ログアウト後

currentUserがNULLになっている所が確認POINTです

感想

簡単にできると思ったログイン認証にハマった。 実際の動きは理解できるんだけど・・・ソースの作りを説明になるとちょっとできない。これは理解していないよね😢

ログアウトはタイムアウト以外のエラーは設定しなくてもいいはず・・・