SignUpができたのでログイン処理を作りたいと思います。
React Firebase入門シリーズ
1-1・ React Firebase入門 初期設定とsignup - masalibの日記
1-2・ 「react-hook-form」を入れてみた - masalibの日記
1-3・ React Firebase入門 signupの通信エラー対応 - masalibの日記
2・React Firebase入門 ログイン処理 今ここ
事前作業
その前にドキュメントルートに(/)アクセスした時、SignUp処理が動いています。
そちらを適当なHomeページを表示したいと思います。そこから
/signup
にアクセスされた時にSignupの画面を表示させます。
Reactの標準のライブラリーに入っていないのが不思議で仕方がない
npm install react-router-dom
をインストールします
react-router-domはルーティングだったりリンクをはったり、リダレクトしたりします。angularからreactに来た人間からするとなぜこの処理が標準じゃないのか未だに理解できないです。
振り分け処理は簡単です。
/ → Home.js
/signup → Signup.js
/login → Login.js
/dashboard → Dashboard
import Signup from "./Signup" import Home from "./Home" import Dashboard from "./Dashboard" import Login from "./Login" import { AuthProvider } from "../contexts/AuthContext" import { BrowserRouter as Router, Switch, Route } from "react-router-dom" function App() { return ( <> <Router> <AuthProvider> <Switch> <Route path="/signup" component={Signup} /> <Route exact path="/" component={Home} /> <Route path="/login" component={Login} /> <Route path="/dashboard" component={Dashboard} /> </Switch> </AuthProvider> </Router> </> ); } export default App;
homeのページは最終的にLPページにしたいのでログインしていない人でもアクセスできるようにしたいです。ログインした場合はdashboardのページを表示させるようにします。 いずれはダッシュボードのページにログインチェックします。 homeは今のところはsingupとログインのリンクが貼っているという状態です。
/src/components/Home.js
import React from 'react' import { Link } from "react-router-dom" const Home = () => { return ( <div> Home: テスト用のリンク(あとで治す) <h2> <Link to="/login">Login</Link> </h2> <h2> <Link to="/signup">signup</Link> </h2> </div> ) } export default Home
結果
デザインもくそもありません
ログインページの処理
signupの処理ができたなら、このページは比較的簡単です。signupと同様に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) } const value = { currentUser, signup, login } useEffect(() => { // Firebase Authのメソッド。ログイン状態が変化すると呼び出される auth.onAuthStateChanged(user => { setCurrentUser(user); setLoading(false) }); }, []); return ( <AuthContext.Provider value={value}> {!loading && children} </AuthContext.Provider> ) }
追加したのはloginという関数
function login(email, password) { return auth.signInWithEmailAndPassword(email, password) }
共通するためのvalueの値を修正
const value = {
currentUser,
signup,
+ login
}
ログインページの作成
ログインページもSignupのページとほぼ変わらないです。 基本的には
- Signup.jsをコピーしてLogin.jsを作る(コピー新規)
- 「Signup」という文字列を「Login」に変える(大文字小文字の区別あり)
- 日本語表記のアカウント作成などをログインに修正する
- ログイン成功時にダッシュボードにリダレクト
- 通信エラーのハンドリング内容を修正する
エラーコード | 概要 |
---|---|
auth/invalid-email | メールアドレスの形式が正しくない |
auth/user-disabled | ユーザーが無効化されている |
auth/user-not-found | ユーザーが見つからない |
auth/wrong-password | パスワードが間違っている |
auth/network-request-failed | 通信エラーまたはタイムアウト |
ただアカウントの作成と違いエラーメッセージはゆるく表示しています。これはアタックされた時に推測されないようにするためです
switch (e.code) { case "auth/network-request-failed": setError("通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。"); break; case "auth/weak-password": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/invalid-email": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/user-not-found": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/user-disabled": setError("入力されたメールアドレスは無効(BAN)になっています。"); break; default: //想定外 setError("ログインに失敗しました。通信環境がいい所で再度やり直してください。"); }
全文はたぶん見なくても大丈夫かと思うけど、みたい人はどうぞ
クリックすると展開されます(長文なので注意)
import React, { useState,useReducer, useEffect } from "react"; import { useForm } from "react-hook-form"; import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; import TextField from "@material-ui/core/TextField"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardActions from "@material-ui/core/CardActions"; import CardHeader from "@material-ui/core/CardHeader"; import Button from "@material-ui/core/Button"; import { useAuth } from "../contexts/AuthContext" import { Link , useHistory} from "react-router-dom" const useStyles = makeStyles((theme: Theme) => createStyles({ container: { display: "flex", flexWrap: "wrap", width: 400, margin: `${theme.spacing(0)} auto` }, loginBtn: { marginTop: theme.spacing(2), flexGrow: 1 }, header: { textAlign: "center", background: "#212121", color: "#fff" }, card: { marginTop: theme.spacing(10) } }) ); //state type type State = { username: string, password: string, isButtonDisabled: boolean, helperText: string, isError: boolean }; const initialState: State = { username: "", password: "", isButtonDisabled: true, helperText: "", isError: false }; type Action = | { type: "setUsername", payload: string } | { type: "setPassword", payload: string } | { type: "setIsButtonDisabled", payload: boolean } | { type: "loginSuccess", payload: string } | { type: "loginFailed", payload: string } | { type: "setIsError", payload: boolean }; const reducer = (state: State, action: Action): State => { switch (action.type) { case "setUsername": return { ...state, username: action.payload }; case "setPassword": return { ...state, password: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "loginSuccess": return { ...state, helperText: action.payload, isError: false }; case "loginFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } }; const Login = () => { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); const [error, setError] = useState("") const { login } = useAuth() const { register, handleSubmit, errors, trigger } = useForm(); const history = useHistory() useEffect(() => { if (state.username.trim() && state.password.trim()){ //trigger(); dispatch({ type: "setIsButtonDisabled", payload: false }); } else { //clearErrors() dispatch({ type: "setIsButtonDisabled", payload: true }); } }, [state.username, state.password]); async function handleLogin (data) { //react-hook-formを導入したためevent -> dataに変更 //event.preventDefault() //react-hook-formを導入したため削除 try { setError("") //sing up ボタンの無効化 dispatch({ type: "setIsButtonDisabled", payload: true }); await login(state.username, state.password) history.push("/dashboard") } catch (e){ //エラーのメッセージの表示 switch (e.code) { case "auth/network-request-failed": setError("通信がエラーになったのか、またはタイムアウトになりました。通信環境がいい所で再度やり直してください。"); break; case "auth/weak-password": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/invalid-email": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/user-not-found": setError("メールアドレスまたはパスワードが間違えています。"); break; case "auth/user-disabled": setError("入力されたメールアドレスは無効(BAN)になっています。"); break; default: //想定外 setError("ログインに失敗しました。通信環境がいい所で再度やり直してください。"); } //dispatch({ // type: "loginFailed", // payload: "Incorrect username or password" //}); //sing up ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.keyCode === 13 || event.which === 13) { if (!state.isButtonDisabled){ handleKeyPresstrigger() if (errors) { //errorメッセージを表示する } else { handleLogin() } } } }; async function handleKeyPresstrigger () { const result = await trigger(); return result } const handleUsernameChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setUsername", payload: event.target.value }); }; const handlePasswordChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setPassword", payload: event.target.value }); }; return ( <form className={classes.container} noValidate autoComplete="off"> <Card className={classes.card}> <CardHeader className={classes.header} title="Login" /> <CardContent> <div> {error && <div style={{ color: "red" }}>{error}</div>} <TextField error={state.isError} fullWidth id="username" name="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} inputRef={register({pattern: /^[A-Za-z0-9]{1}[A-Za-z0-9_.-]*@{1}[A-Za-z0-9_.-]{1,}\.[A-Za-z0-9]{1,}$/ })} /> {errors.username?.type === "pattern" && <div style={{ color: "red" }}>メールアドレスの形式で入力されていません</div>} <TextField error={state.isError} fullWidth id="password" name="password" type="password" label="Password" placeholder="Password" margin="normal" onChange={handlePasswordChange} onKeyPress={handleKeyPress} inputRef={register({ required: true, minLength: 6 })} /> {errors.password?.type === "minLength" && <div style={{ color: "red" }}>パスワードは6文字以上で入力してください</div>} </div> もしアカウントがないなら<Link to="/signup">こちら</Link>からアカウントを作成してください </CardContent> <CardActions> <Button variant="contained" size="large" color="secondary" className={classes.loginBtn} onClick={handleSubmit(handleLogin)} disabled={state.isButtonDisabled} > Login </Button> </CardActions> </Card> </form> ); }; export default Login;
結果
ログインが成功するとダッシュボードにリダレクトされます
感想
SignUPを作った時に比べる簡単にできた。でもSignUpとLoginってほぼ同じような画面なのに2つファイルで管理している・・・作りとしてはあまりよくないのかも・・・
このあとはダッシュボードにログアウトと現在のユーザーの情報を出したいです。