この記事では、図で示したような ToDo アプリの開発手順について全 4 部構成で詳しく解説しています。React の基礎から応用までを実際のコーディングを通じて学ぶことに焦点を当てています。初心者から中級者向けの内容となっており、ToDo アプリの開発は、実践的な技能の習得とポートフォリオ作成のための具体的な例として最適だと考えています。
この第 4 部では、Firebase による認証機能の実装を行います。
想定読者
- react の初学者から中級者
- react を触ったことはあるが、なにかポートフォリオ的な物を作りたい人
記事構成
Firebase Authentication とは
Firebase Authentication は、Firebase プラットフォームの一部であり、アプリケーションに安全なユーザー認証機能を簡単に追加できるサービスです。このサービスを使用することで、開発者はユーザーのサインインとサインアップのプロセスを簡素化することができます。
具体的な特徴は以下のとおりです
- 多様な認証方法
Firebase Authentication は、メールアドレスとパスワードによる認証、Google、Facebook、Twitter、GitHub などのソーシャルメディアアカウントを使用した認証、電話番号を使用した SMS 認証など、多様な認証方法をサポートしています。 - ユーザー管理
Firebase コンソールを通じて、登録されたユーザーの管理が容易に行えます。ユーザー情報の確認や、ユーザーの有効・無効化などの操作が可能です。 - web アプリへの統合
Firebase SDK を利用することで、Web アプリケーションに簡単に認証機能を統合できます。
他にも様々な特徴がありますので、詳しく確認したい方は公式サイトをご確認ください。
Firebase プロジェクトの作成
ここからは Firebase のアカウントを持っている前提で話を進めます。Firebaseにアクセスします。
1. プロジェクトを追加をクリックする
2. プロジェクトに名前をつける
3. Google アナリティクスの設定を無効化する
アクセス数などの統計データの取得は必要ないので、アナリティクスの設定を無効化します。
4. 続行を押す
5. ウェブアプリに Firebase を追加する
左から 3 番目の web をクリックします。
任意の名前でアプリのニックネームをつけて、アプリを登録します。
以下の内容を控えておきます。(コンソール上から再確認可能)
6. 認証方法を設定する
Authentication をクリックして、認証方法を設定します。表示された画面で「始める」を押します。
ログイン方法を設定する
「ログイン方法を設定」をクリックして、ログイン方法を選択します。
今回はメール/パスワードにします。有効にするをチェックして、保存を押します。
Firebase SDK の導入
- Firebase SDK をインストールする
ターミナル上で frontend ディレクトリに移動し、以下のコマンドを実行します。
npm install firebase
- .env ファイルを作成する
frontend ディレクトリの直下に.env ファイルを作成します。XXXXXXXXX には Firebase プロジェクトの作成の 5 で控えた値がそれぞれ入ります。REACT_APP_FIREBASE_API_KEY=XXXXXXXXX REACT_APP_FIREBASE_AUTH_DOMAIN=XXXXXXXXX REACT_APP_FIREBASE_PROJECT_ID=XXXXXXXXX REACT_APP_FIREBASE_STORAGE_BUCKET=XXXXXXXXX REACT_APP_FIREBASE_MESSAGING_SENDER_ID=XXXXXXXXX REACT_APP_FIREBASE_APP_ID=XXXXXXXXX
- firebase.ts ファイルを作成する
src ディレクトリに auth をフォルダを作成し、firebase.ts ファイルを作成します。process.env によって先程作成した.evn の内容を取得しています。
// firebase.ts import { initializeApp } from "firebase/app"; import { getAuth } from "firebase/auth"; import { getFirestore } from "firebase/firestore"; import { getStorage } from "firebase/storage"; const firebaseConfig = { apiKey: process.env.REACT_APP_FIREBASE_API_KEY, authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN, projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID, storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET, messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID, appId: process.env.REACT_APP_FIREBASE_APP_ID, }; const app = initializeApp(firebaseConfig); export const storage = getStorage(app); export const auth = getAuth(app); export const db = getFirestore(app);
- AuthContext.tsx を作成する
Firebase 認証の状態を管理するための AuthContext という名前の認証コンテキストを作成します。このコンテキストを使用することで、アプリケーションのどの部分からでも現在のユーザー認証の状態にアクセスできるようになります。
// AuthContext.tsx import React, { createContext, useState, useContext, useEffect, } from "react"; import { auth } from "./firebase"; import { User } from "firebase/auth"; // AuthContextの型を定義 interface AuthContextProps { user: User | null; isLoading: boolean; } const AuthContext = createContext<AuthContextProps>({ user: null, isLoading: true, }); export const useAuthContext = () => { return useContext(AuthContext); }; export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children, }) => { const [user, setUser] = useState<User | null>(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { const unsubscribe = auth.onAuthStateChanged((user) => { setUser(user); setIsLoading(false); }); return unsubscribe; // クリーンアップ関数 }, []); return ( <AuthContext.Provider value={{ user, isLoading }}> {children} </AuthContext.Provider> ); };
認証機能の導入
- Loading 処理を追加する
src ディレクトリ内の components フォルダに Loading.tsx ファイルを作成します。認証が完了するまでローディングを表示するために利用します。
// Loading.tsx import { Box, CircularProgress } from "@mui/material"; import React from "react"; interface LoadingProps { height?: string; } const Loading: React.FC<LoadingProps> = ({ height = "100vh" }) => { return ( <Box display="flex" justifyContent="center" alignItems="center" height={height} > <CircularProgress /> </Box> ); }; export default Loading;
- プライベートルートを作成する
auth フォルダ内に PrivateRoute.tsx ファイルを作成します。このコンポーネントは、先ほど作成した useAuthContext フックを使用してユーザー情報を取得し、ユーザーが認証されている場合は指定されたページに遷移を許可します。ユーザーが認証されていない場合は、サインインページにリダイレクトさせます。
// PrivateRoute.tsx import React from "react"; import { useAuthContext } from "./AuthContext"; import Loading from "../components/Loading"; import { Navigate } from "react-router-dom"; interface PrivateRouteProps { element: React.ReactElement; } export const PrivateRoute: React.FC<PrivateRouteProps> = ({ element }) => { const { user, isLoading } = useAuthContext(); if (isLoading) { return <Loading />; } if (user) { return element; } return <Navigate to="/signin" />; };
- サインインページを作成する
auth フォルダ内に Signin.tsx ファイルを作成します。Firebase の Authentication を使用してメールアドレスとパスワードによる認証を実装するため、これらの入力フィールドを含むログインフォームを実装します。
// Signin.tsx import { Avatar, Box, Button, Grid, Paper, Stack, Typography, } from "@mui/material"; import { useFormik } from "formik"; import React, { useLayoutEffect } from "react"; import * as Yup from "yup"; import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; import { signInWithEmailAndPassword } from "firebase/auth"; import { auth } from "./firebase"; import { Link, useNavigate } from "react-router-dom"; import { useAuthContext } from "./AuthContext"; import Loading from "../components/Loading"; import { FormikTextField } from "../components/FormikTextField"; const Signin: React.FC = () => { const { user, isLoading } = useAuthContext(); const navigate = useNavigate(); const validationSchema = Yup.object().shape({ email: Yup.string().required("Email is required"), password: Yup.string().required("Password is required"), }); const formik = useFormik({ initialValues: { email: "", password: "", }, validationSchema, onSubmit: async (state) => { signInWithEmailAndPassword(auth, state.email, state.password) .then((user) => { navigate("/"); }) .catch((error) => { console.error(error); }); }, }); useLayoutEffect(() => { if (user) { navigate("/"); } }, [user, navigate]); if (isLoading) { return <Loading />; } return ( <Grid> <Paper elevation={3} sx={{ p: 4, height: "100%", width: "280px", m: "20px auto", }} > <Box sx={{ width: "100%", display: "flex", justifyContent: "center" }} > <Avatar> <LockOutlinedIcon /> </Avatar> </Box> <Typography>Signin</Typography> <form noValidate onSubmit={formik.handleSubmit}> <Stack spacing={2}> <FormikTextField name="email" label="Email *" variant="standard" formik={formik} /> <FormikTextField name="password" label="Password *" variant="standard" type="password" formik={formik} /> <Button fullWidth variant="contained" type="submit"> Login </Button> <Link to={"/signup"}>Create account</Link> </Stack> </form> </Paper> </Grid> ); }; export default Signin;
- サインアップページを作成する
auth フォルダ内に Signup.tsx ファイルを作成し、新規ユーザー登録のためのフォームを実装します。このフォームでは、ユーザーがメールアドレスとパスワードを入力してアカウントを作成できるようにします。Firebase Authentication を利用して、入力された情報に基づいて新規ユーザーの登録を行い、成功した場合はログインページにリダイレクトさせます。
// Signup.tsx import { Avatar, Box, Button, Grid, Paper, Stack, Typography, } from "@mui/material"; import { useFormik } from "formik"; import React, { useLayoutEffect } from "react"; import * as Yup from "yup"; import LockOutlinedIcon from "@mui/icons-material/LockOutlined"; import { createUserWithEmailAndPassword } from "firebase/auth"; import { auth } from "./firebase"; import { Link, useNavigate } from "react-router-dom"; import { useAuthContext } from "./AuthContext"; import Loading from "../components/Loading"; import { FormikTextField } from "../components/FormikTextField"; const Signup: React.FC = () => { const { user, isLoading } = useAuthContext(); const navigate = useNavigate(); const validationSchema = Yup.object().shape({ email: Yup.string().required("Email is required"), password: Yup.string().required("Password is required"), }); const formik = useFormik({ initialValues: { email: "", password: "", }, validationSchema, onSubmit: async (state) => { await createUserWithEmailAndPassword( auth, state.email, state.password ); navigate("/"); }, }); useLayoutEffect(() => { if (user) { navigate("/"); } }, [user, navigate]); if (isLoading) { return <Loading />; } return ( <Grid> <Paper elevation={3} sx={{ p: 4, height: "100%", width: "280px", m: "20px auto", }} > <Box sx={{ width: "100%", display: "flex", justifyContent: "center", }} > <Avatar> <LockOutlinedIcon /> </Avatar> </Box> <Typography>Signup</Typography> <form noValidate onSubmit={formik.handleSubmit}> <Stack spacing={2}> <FormikTextField name="email" label="Email *" variant="standard" formik={formik} /> <FormikTextField name="password" label="Password *" variant="standard" type="password" formik={formik} /> <Button fullWidth variant="contained" type="submit"> Submit </Button> <Link to={"/signin"}>Or Sign in</Link> </Stack> </form> </Paper> </Grid> ); }; export default Signup;
- プライベートルートを適用する
プライベートルートを適用し、認証されたユーザーのみが特定のページにアクセスできるようにするために、App.tsx を修正します。この変更により、認証されていないユーザーが保護されたページにアクセスしようとした場合、ログインページへリダイレクトされます。// App.tsx import "./App.css"; import { Route, Routes, useLocation } from "react-router-dom"; import AppLayout from "./layout/AppLayout"; import { CssBaseline, ThemeProvider } from "@mui/material"; import { createTheme } from "./theme"; import TodoList from "./todo/TodoList"; import { useTodos } from "./todo/hooks/useTodos"; import { BackgroundLocation } from "./todo/hooks/useModalRoute"; import TodoEdit from "./todo/components/TodoEdit"; import Signup from "./auth/Signup"; import Signin from "./auth/Signin"; import { PrivateRoute } from "./auth/PrivateRoute"; function App() { const theme = createTheme(); const { todos, setTodos } = useTodos(); const location = useLocation(); const background = (location.state as BackgroundLocation)?.background; return ( <div className="App"> <ThemeProvider theme={theme}> <CssBaseline /> <Routes location={background || location}> <Route path="/" element={<PrivateRoute element={<AppLayout />} />}> <Route index element={ <TodoList todos={todos} setTodos={setTodos} filterFunction={(todo) => !todo.done} /> } /> <Route path="/important" element={ <TodoList todos={todos} setTodos={setTodos} filterFunction={(todo) => todo.important} /> } /> <Route path="/all" element={ <TodoList todos={todos} setTodos={setTodos} filterFunction={() => true} /> } /> <Route path="/complited" element={ <TodoList todos={todos} setTodos={setTodos} filterFunction={(todo) => todo.done} /> } /> <Route path="/todos/:id" element={ <TodoList todos={todos} setTodos={setTodos} filterFunction={(todo) => !todo.done} /> } /> </Route> <Route path="/signup" element={<Signup />} /> <Route path="/signin" element={<Signin />} /> </Routes> {background && ( <Routes> <Route path="/todos/:id" element={<TodoEdit todos={todos} setTodos={setTodos} />} /> </Routes> )} </ThemeProvider> </div> ); } export default App;
- サインアウト機能を追加する
auth フォルダ内に Signout.tsx ファイルを作成し、サインアウトできる機能を実装します。
// Signout.tsx import React from "react"; import { signOut } from "firebase/auth"; import { auth } from "./firebase"; import { List, ListItem, ListItemButton, ListItemIcon, ListItemText, } from "@mui/material"; import LogoutIcon from "@mui/icons-material/Logout"; const Signout: React.FC = () => { const handleSignout = () => { signOut(auth); }; return ( <List> <ListItem key="signout" disablePadding> <ListItemButton onClick={handleSignout}> <ListItemIcon> <LogoutIcon /> </ListItemIcon> <ListItemText primary="Signout" /> </ListItemButton> </ListItem> </List> ); }; export default Signout;
作成した機能をサイドバーに配置します。layout フォルダ内の Sidebar.tsx を以下のように修正します。
// Sidebar.tsx import { Box, Divider, List, ListItem, ListItemButton, ListItemIcon, ListItemText, Toolbar, } from "@mui/material"; import React from "react"; import StarIcon from "@mui/icons-material/Star"; import TaskAltIcon from "@mui/icons-material/TaskAlt"; import AllInclusiveIcon from "@mui/icons-material/AllInclusive"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import { useNavigate } from "react-router-dom"; import Signout from "../auth/Signout"; const Sidebar: React.FC = () => { const navigate = useNavigate(); const listItems = [ { text: "Todos", icon: <FormatListBulletedIcon />, to: "/", }, { text: "Important", icon: <StarIcon />, to: "/important", }, { text: "All", icon: <AllInclusiveIcon />, to: "/all", }, { text: "Complited", icon: <TaskAltIcon />, to: "complited", }, ]; const handleItemClick = (to: string) => { navigate(to); }; return ( <Box component="nav" sx={{ width: { sm: "280px" }, height: "100vh", borderRight: 1, // 1ピクセルの右境界線を追加 borderColor: "divider", // デフォルトの境界線色を使用 bgcolor: "secondary.main", color: "secondary.contrastText", display: "flex", flexDirection: "column", justifyContent: "space-between", }} > <Box> <Toolbar variant="regular" sx={{ fontSize: "22px" }}> ToDo App </Toolbar> <Divider /> <List> {listItems.map((item, index) => ( <ListItem key={item.text} disablePadding> <ListItemButton onClick={() => handleItemClick(item.to)}> <ListItemIcon>{item.icon}</ListItemIcon> <ListItemText primary={item.text} /> </ListItemButton> </ListItem> ))} </List> <Divider /> </Box> <Box sx={{ pb: 2 }}> <Signout /> </Box> </Box> ); }; export default Sidebar;
次のステップ
もっと体系的に勉強したい方や他にもアプリケーションを作ってみたいという方、バックエンドと組み合わせてフルスタックな実装をしたい方は下記のコースや書籍をおすすめします。
まとめ
この記事では、React と TypeScript を使用した ToDo アプリ開発シリーズの第 4 部を紹介しました。第4部ではFirebaseによる認証機能の実装を行いました。認証されている場合のみ、各ページにアクセスでき、認証されていない場合は、サインインページにリダイレクトされるように実装しました。
これでToDoアプリ開発の実装は終了です。CRUD処理をはじめとする基本的な処理の実装スキルが少しでも身につき、コーディングのイメージが湧いていれば幸いです。お疲れ様でした。