アプリを作るなら「Firebase!!」と聞いた事があった
アプリ開発をあまりしたことがなく業務では触らせてくれない。なんとなくは便利なのは知っているんだけど実際に動くアプリを作って理解しようと思った。お決まりのCHATアプリなんかも作ってみたい。 兎にも角にもアプリの基礎となるユーザー認証の部分をやる事にしました。
- 利用している技術要素
- Reactの初期設定
- Firebaseの初期設定
- 環境ファイルの確認
- 設定ファイルの内容をFirebaseのコンポーネントに読み込ませる
- signUP画面の作成
- SignUPの機能を追加
- signUP画面の修正
- 結果
- 感想
- 参考ソース
利用している技術要素
- Firebase Authentication
- React
- React Hooks(useContext, useState, useEffect)
masalib.hatenablog.com
masalib.hatenablog.com
masalib.hatenablog.com - Material UI
masalib.hatenablog.com
masalib.hatenablog.com
masalib.hatenablog.com
このFirebaseをやるために、色々と勉強してきた。
Reactの初期設定
create-raect-appで環境を作成するといらないCSSやロゴなどがある。 開発には必要ないので削除する
/src/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; ReactDOM.render( <React.StrictMode> <App /> </React.StrictMode>, document.getElementById('root') );
/src/App.js
function App() { return ( <div className="App"> Hello,World </div> ); } export default App;
初期の状態でのブランチをきりました
GitHub - masalib/Learn-Firebase at create-raect-app-initial-delete
Firebaseの初期設定
アプリで使うためにはプロジェクトとAPIキーと呼ばれいるものが必要です。
プロジェクトがない前提です。また2020/11/22の時点での画面です。UIがちょくちょく変わるので注意です。
プロジェクトを作成する。今後も使っていくならわかりやすい名前にした方がいいです。
Googleアナリティクスの設定を有効にする(開発だけならいらないかも)
Googleアナリティクスのアカウントの選択画面がでるので該当のユーザーを設定する
プロジェクトができるのでAuthenticationを選択して始めるのボタンを押す
Sign-in methodを選択してメール/パスワードを選択する
有効化にする(メールリンクは有効化しない)
プロジェクトの設定の全般を開く
マイアプリの設定で「</>」になっているアイコンを開く
マイアプリの名前の設定
APIキーのコピー
reactのプロジェクトに戻ってきて「.env.local」または「.env.development.local」のファイルを作成してAPIキーなどを設定します。もしreactのプロジェクトが起動している場合はrestartする(環境設定ファイルを読み込むため)
sample
REACT_APP_APIKEY= REACT_APP_AUTHDOMAIN= REACT_APP_DATABASEURL= REACT_APP_PROJECT_ID= REACT_APP_STORAGE_BUCKET= REACT_APP_MESSAGING_SENDER_ID= REACT_APP_APP_ID= REACT_APP_MEASUREMENT_ID=
環境ファイルの確認
設定ファイルなので読み込ませれば出力しました /src/App.js
function App() { return ( <> <div>Hello,World </div> + <div>{`REACT_APP_APIKEY:${process.env.REACT_APP_APIKEY}`}</div> + <div>{`REACT_APP_AUTHDOMAIN:${process.env.REACT_APP_AUTHDOMAIN}`}</div> + <div>{`REACT_APP_DATABASEURL:${process.env.REACT_APP_DATABASEURL}`}</div> + <div>{`REACT_APP_PROJECT_ID:${process.env.REACT_APP_PROJECT_ID}`}</div> + <div>{`REACT_APP_STORAGE_BUCKET:${process.env.REACT_APP_STORAGE_BUCKET}`}</div> + <div>{`REACT_APP_MESSAGING_SENDER_ID:${process.env.REACT_APP_MESSAGING_SENDER_ID}`}</div> + <div>{`REACT_APP_APP_ID:${process.env.REACT_APP_APP_ID}`}</div> + <div>{`REACT_APP_MEASUREMENT_ID:${process.env.REACT_APP_MEASUREMENT_ID}`}</div> </> ); }
ここでAPIキーが出力されればOKです。
設定ファイルの内容をFirebaseのコンポーネントに読み込ませる
Firebaseの接続につかうのはFirebaseになります。他もあるのですが今回はこれにしました
npm install Firebase
コンポーネントを読み込むプログラムの作成
/src/firebase.js
import firebase from "firebase/app" import "firebase/auth" const app = firebase.initializeApp({ apiKey: process.env.REACT_APP_APIKEY, authDomain: process.env.REACT_APP_AUTHDOMAIN, databaseURL: process.env.REACT_APP_DATABASEURL, projectId: process.env.REACT_APP_PROJECT_ID, storageBucket: process.env.REACT_APP_STORAGE_BUCKET, messagingSenderId: process.env.REACT_APP_MESSAGING_SENDER_ID, appId: process.env.REACT_APP_APP_ID, measurementId: process.env.REACT_APP_MEASUREMENTID }); export const auth = app.auth() export default app
お恥ずかしいのですがファイルを作成した時点のコミットファイルはバグがあり修正しました
signUP画面の作成
前回のLogin画面と同じです
/src/components/Signup.js
クリックすると展開されます(長文なので注意)
import React, { useReducer, useEffect } from "react"; 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"; const useStyles = makeStyles((theme: Theme) => createStyles({ container: { display: "flex", flexWrap: "wrap", width: 400, margin: `${theme.spacing(0)} auto` }, signupBtn: { 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, passwordconfirm: string, isButtonDisabled: boolean, helperText: string, isError: boolean }; const initialState: State = { username: "", password: "", passwordconfirm: "", isButtonDisabled: true, helperText: "", isError: false }; type Action = | { type: "setUsername", payload: string } | { type: "setPassword", payload: string } | { type: "setPasswordConfirm", payload: string } | { type: "setIsButtonDisabled", payload: boolean } | { type: "signupSuccess", payload: string } | { type: "signupFailed", 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 "setPasswordConfirm": return { ...state, passwordconfirm: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "signupSuccess": return { ...state, helperText: action.payload, isError: false }; case "signupFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } }; const Signup = () => { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); useEffect(() => { if (state.username.trim() && state.password.trim() && state.passwordconfirm.trim() ) { dispatch({ type: "setIsButtonDisabled", payload: false }); } else { dispatch({ type: "setIsButtonDisabled", payload: true }); } }, [state.username, state.password, state.passwordconfirm]); const handleSignup = () => { if (state.username === "abc@email.com" && state.password === "password") { dispatch({ type: "signupSuccess", payload: "Signup Successfully" }); } else { dispatch({ type: "signupFailed", payload: "Incorrect username or password" }); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.keyCode === 13 || event.which === 13) { state.isButtonDisabled || handleSignup(); } }; 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 }); }; const handlePasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setPasswordConfirm", payload: event.target.value }); }; return ( <form className={classes.container} noValidate autoComplete="off"> <Card className={classes.card}> <CardHeader className={classes.header} title="Sign UP " /> <CardContent> <div> <TextField error={state.isError} fullWidth id="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password" type="password" label="Password" placeholder="Password" margin="normal" helperText={state.helperText} onChange={handlePasswordChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password-confirm" type="password" label="Password-confirm" placeholder="Password-confirm" margin="normal" helperText={state.helperText} onChange={handlePasswordConfirmChange} onKeyPress={handleKeyPress} /> </div> もしアカウントがあるなら Log In </CardContent> <CardActions> <Button variant="contained" size="large" color="secondary" className={classes.signupBtn} onClick={handleSignup} disabled={state.isButtonDisabled} > Signup </Button> </CardActions> </Card> </form> ); }; export default Signup;
ログインと違いパスワードの確認で2回入力させる点が違います
SignUPの機能を追加
SignUP機能をSignUPの画面に直接記載してもよかったがContextで共有させるようにしています
Contextの設定
Firebaseとやり取りする部分と画面に共有したい変数を設定しています。現時点ではcurrentUser(変数)と signup(関数)を共有します
ソース
/src/contexts/AuthContext.js
クリックすると展開されます(長文なので注意)
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) } const value = { currentUser, signup } useEffect(() => { // Firebase Authのメソッド。ログイン状態が変化すると呼び出される auth.onAuthStateChanged(user => { setCurrentUser(user); }); }, []); return ( <AuthContext.Provider value={value}> {children} </AuthContext.Provider> ) }
ソース解説
Contextを指定します。
export function useAuth() { return useContext(AuthContext) }
signupするための関数作成しています
function signup(email, password) { return auth.createUserWithEmailAndPassword(email, password) }
currentUser(変数)とsignup(関数)を共有するために1つにまとめています
const value = { currentUser, signup }
app.jsなどで共有する範囲について指定しています。valueが共有する部分になります
<AuthContext.Provider value={value}> {children} </AuthContext.Provider>
なぜ記述が必要なのかまだ理解していない部分
useEffect(() => { // Firebase Authのメソッド。ログイン状態が変化すると呼び出される auth.onAuthStateChanged(user => { setCurrentUser(user); }); }, []);
別の参考ソースは以下です
useEffect(() => { const unsubscribe = auth.onAuthStateChanged(user => { setCurrentUser(user) setLoading(false) }) return unsubscribe }, [])
参考にしたソースでは記載しているが自分ではなぜ必要なのかよくわかっていません currentユーザーを更新する?? すべての変数を監視するの?
Contextを読み込む
/src/components/App.js
import Signup from "./Signup" import { AuthProvider } from "../contexts/AuthContext" function App() { return ( <> <AuthProvider> <Signup /> </AuthProvider> </> ); } export default App;
以下の範囲が共有できる部分です
<AuthProvider> <Signup /> </AuthProvider>
これでSignupのコンポーネント内でContextが使えます
説明を省いていますが /src/App.js → /src/components/App.jsに移動しました
signUP画面の修正
実際にSignUPを起動するように修正します
ソース
/src/components/Signup.js
クリックすると展開されます(長文なので注意)
import React, { useState,useReducer, useEffect } from "react"; 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" const useStyles = makeStyles((theme: Theme) => createStyles({ container: { display: "flex", flexWrap: "wrap", width: 400, margin: `${theme.spacing(0)} auto` }, signupBtn: { 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, passwordconfirm: string, isButtonDisabled: boolean, helperText: string, isError: boolean }; const initialState: State = { username: "", password: "", passwordconfirm: "", isButtonDisabled: true, helperText: "", isError: false }; type Action = | { type: "setUsername", payload: string } | { type: "setPassword", payload: string } | { type: "setPasswordConfirm", payload: string } | { type: "setIsButtonDisabled", payload: boolean } | { type: "signupSuccess", payload: string } | { type: "signupFailed", 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 "setPasswordConfirm": return { ...state, passwordconfirm: action.payload }; case "setIsButtonDisabled": return { ...state, isButtonDisabled: action.payload }; case "signupSuccess": return { ...state, helperText: action.payload, isError: false }; case "signupFailed": return { ...state, helperText: action.payload, isError: true }; case "setIsError": return { ...state, isError: action.payload }; default: return state; } }; const Signup = () => { const classes = useStyles(); const [state, dispatch] = useReducer(reducer, initialState); const [error, setError] = useState("") const [successMessage, setSuccessMessage] = useState("") const { signup } = useAuth() useEffect(() => { if (state.password.trim() !== state.passwordconfirm.trim()){ dispatch({ type: "setIsButtonDisabled", payload: true }); } else if (state.username.trim() && state.password.trim()){ dispatch({ type: "setIsButtonDisabled", payload: false }); } else { dispatch({ type: "setIsButtonDisabled", payload: true }); } }, [state.username, state.password, state.passwordconfirm]); async function handleSignup (event) { event.preventDefault() try { setError("") setSuccessMessage("") //sing up ボタンの無効化 dispatch({ type: "setIsButtonDisabled", payload: true }); await signup(state.username, state.passwordconfirm) dispatch({ type: "signupSuccess", payload: "Signup Successfully" }); //sing up ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); setSuccessMessage("アカウントの作成に成功しました") } catch { setError("Failed to create an account") dispatch({ type: "signupFailed", payload: "Incorrect username or password" }); //sing up ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); } }; const handleKeyPress = (event: React.KeyboardEvent) => { if (event.keyCode === 13 || event.which === 13) { state.isButtonDisabled || handleSignup(); } }; 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 }); }; const handlePasswordConfirmChange: React.ChangeEventHandler<HTMLInputElement> = ( event ) => { dispatch({ type: "setPasswordConfirm", payload: event.target.value }); }; return ( <form className={classes.container} noValidate autoComplete="off"> <Card className={classes.card}> <CardHeader className={classes.header} title="Sign UP " /> <CardContent> <div> {error && <div variant="danger">{error}</div>} {successMessage && <div variant="danger">{successMessage}</div>} <TextField error={state.isError} fullWidth id="username" type="email" label="Username" placeholder="Username" margin="normal" onChange={handleUsernameChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password" type="password" label="Password" placeholder="Password" margin="normal" helperText={state.helperText} onChange={handlePasswordChange} onKeyPress={handleKeyPress} /> <TextField error={state.isError} fullWidth id="password-confirm" type="password" label="Password-confirm" placeholder="Password-confirm" margin="normal" helperText={state.helperText} onChange={handlePasswordConfirmChange} onKeyPress={handleKeyPress} /> </div> もしアカウントがあるなら Log In </CardContent> <CardActions> <Button variant="contained" size="large" color="secondary" className={classes.signupBtn} onClick={handleSignup} disabled={state.isButtonDisabled} > Signup </Button> </CardActions> </Card> </form> ); }; export default Signup;
ソース解説
contextで設定した関数を使えるようにします
import { useAuth } from "../contexts/AuthContext" ・ ・ ・ const { signup } = useAuth()
実際にSignupさせるhandleSignupです
- 通信処理させるのでfunctionにasyncを設定しています。実際に通信させるときはawaitを加えます。
- 最終的には必要ないのですが、SignUP成功時にメッセージを設定しています
- tryキャッチでローディング中およびタイムアウトを設定しています。タイムアウトになるとnetwork-request-failedになる・・・何秒なのかわからない。
- 本来はエラーコードとか出力したりログに吐き出さないといけないだけどその部分はできていない
エラーについて
https://firebase.google.com/docs/reference/js/firebase.auth.Error
async function handleSignup (event) { event.preventDefault() try { setError("") setSuccessMessage("") //sing up ボタンの無効化 dispatch({ type: "setIsButtonDisabled", payload: true }); await signup(state.username, state.passwordconfirm) dispatch({ type: "signupSuccess", payload: "Signup Successfully" }); //sing up ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); setSuccessMessage("アカウントの作成に成功しました") } catch { setError("Failed to create an account") dispatch({ type: "signupFailed", payload: "Incorrect username or password" }); //sing up ボタンの有効化 dispatch({ type: "setIsButtonDisabled", payload: false }); } };
結果
Firebaseの完了画面にユーザーが追加されています
感想
長文になったがSignupができてよかった。 色々と課題もあるがログインなども引き続きやります
参考ソース
https://github.com/WebDevSimplified/React-Firebase-Auth
Typescript×React×Hooksで会員管理①Firebase Authで認証基盤外出し - Qiita