masalibの日記

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

React Firebase入門 初期設定とsignup

アプリを作るなら「Firebase!!」と聞いた事があった

www.youtube.com

アプリ開発をあまりしたことがなく業務では触らせてくれない。なんとなくは便利なのは知っているんだけど実際に動くアプリを作って理解しようと思った。お決まりのCHATアプリなんかも作ってみたい。 兎にも角にもアプリの基礎となるユーザー認証の部分をやる事にしました。

利用している技術要素

この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がちょくちょく変わるので注意です。

  1. プロジェクトを作成する。今後も使っていくならわかりやすい名前にした方がいいです。

    プロジェクトの作成

  2. Googleアナリティクスの設定を有効にする(開発だけならいらないかも)

    Googleアナリティクスの設定

  3. Googleアナリティクスのアカウントの選択画面がでるので該当のユーザーを設定する

    アナリティクスのユーザー設定

  4. プロジェクトができるのでAuthenticationを選択して始めるのボタンを押す

    Authenticationを開始

  5. Sign-in methodを選択してメール/パスワードを選択する

    Sign-in method

  6. 有効化にする(メールリンクは有効化しない)

    メール/パスワードの有効化

  7. プロジェクトの設定の全般を開く

    プロジェクトの設定の全般を開く

  8. マイアプリの設定で「</>」になっているアイコンを開く

    マイアプリの設定

  9. マイアプリの名前の設定

    マイアプリの名前の設定

  10. APIキーのコピー

  11. 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画面と同じです

masalib.hatenablog.com

/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
        });
        
    }
};

結果

f:id:masalib:20201123003756g:plain
結果

Firebaseの完了画面にユーザーが追加されています

f:id:masalib:20201123003855p:plain

感想

長文になったがSignupができてよかった。 色々と課題もあるがログインなども引き続きやります

参考ソース

https://github.com/WebDevSimplified/React-Firebase-Auth
Typescript×React×Hooksで会員管理①Firebase Authで認証基盤外出し - Qiita