Lesson 10

Next.jsとFirebaseでTODOアプリの開発 - 画面・処理の実装

Lesson 10 Chapter 1
ログイン画面の実装

はじめに

nextjs-10-1 本チャプターでセットアップしていく部分

本チャプターではFirebase Authenticationを使用した認証処理を実装していきます。
まず初めにボタン等を配置した画面を作成し次にボタン等の処理を記述していくという風な流れで実装していきます。

画面の作成

初めに認証処理を実装する画面を作成していきます。
ログインボタンとログアウトボタンを表示する画面を作成していきます。 pagesの中のindex.jsxの中身を削除し、下記を記述します。

pages/index.jsx
export default function Index() {
    return (
        <div style={{textAlign: 'center'}}>
            <h1>TODOアプリケーション</h1>
            <div>
                <h2>ログインしています。</h2>
                <button>ログアウト</button>
            </div>
            <div>
                <h2>ログインしていません。</h2>
                <button>ログイン</button>
            </div>
        </div>
    );
}
nextjs-10-2

今はログインボタンとログアウトボタンどちらも表示されていますが、 認証処理を実装していく中でログインをしていなければログインボタンを表示し、ログイン済みであればログアウトボタンを表示する処理を記述していきます。
それでは認証処理の実装を行なっていきます。

認証処理の実装

FirebaseSDK初期化ファイルの追記

まず初めにFirebaseSDKの初期化ファイルで認証機能を使用するメソッドをexportしていきます。

firebase.js
import { getFirestore } from "firebase/firestore";
import { getApps, initializeApp } from "firebase/app";
import { getAuth } from "firebase/auth";//追加

const firebaseConfig = {
    apiKey: process.env.NEXT_PUBLIC_FIREBASE_APIKEY,
    authDomain: process.env.NEXT_PUBLIC_FIREBASE_DOMAIN,
    projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
    storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
    messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_SENDER_ID,
    appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID,
};

if (!getApps()?.length) {
    initializeApp(firebaseConfig);
}

export const db = getFirestore();
export const auth = getauth();//追加

firebase.jsでexportしたauthを使用して認証処理を実装していきます。 その前にTODOアプリ内でユーザの状態と認証処理をexportするコンポーネントを作成していきます。

ユーザの状態を管理するコンポーネントの作成

アプリ内で共通で使用するユーザの状態と認証処理をexportするコンポーネントを作成します。 componentsディレクトリを新規作成し、AuthContext.jsを作成します。

components/AuthContext.js
import { GoogleAuthProvider, signInWithRedirect } from "firebase/auth";
import { createContext, useContext, useEffect, useState } from "react";
import { auth } from "../firebase";

const AuthContext = createContext();

export const useAuth = () => {
    return useContext(AuthContext);
};

const AuthProvider = ({ children }) => {
    const [currentUser, setCurrentUser] = useState(null);
    const [loading, setLoading] = useState(true);

    const login = () => {
        const provider = new GoogleAuthProvider();
        return signInWithRedirect(auth, provider);
    };

    const logout = () => {
        return auth.signOut();
    };

    useEffect(() => {
        return auth.onAuthStateChanged((user) => {
            setCurrentUser(user);
            setLoading(false);
        });
    }, []);

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

    return (
        <AuthContext.Provider value={value}>
            {loading ? <p>loading...</p> : children}
        </AuthContext.Provider>
    );
};

export default AuthProvider;

上記のファイルではコンテキストを作成し, 実際のログイン, ログアウト, ログイン状態の確認は useAuth を通して使用できるようにしています。
usestateのCurrentUserではオブジェクトかnullかでログインしているかしていないかを判断できます。
userEffectではauthのonAuthStateChangedメソッドを使用してページ読み込み毎にCurrentUserを最新の状態に更新しています。
更新中はLoadingの文字を表示させるようにしておきます。
またアプリ全体で useAuth で渡す値を使用したいので, MyApp コンポーネントの返すコンポーネントとして記述します。

pages/_app.js
import AuthProvider from "../components/AuthContext";
import "../styles/globals.css";

function MyApp({ Component, pageProps }) {
    return (
        <AuthProvider>
            <Component {...pageProps} />
        <AuthProvider>
    );
}

export default MyApp;
                    

画面の処理の実装

それでは最初に作成した画面の処理を実装していきます。 下記の様に記述してください。

pages/index.jsx
import { useAuth } from "../components/AuthContext";

export default function Index() {
    const { currentUser, login, logout } = useAuth();

    const handleLoginButton = () => {
        login();
    };

    const handleLogoutButton = () => {
        logout();
    };
    return (
        <div style={{ textAlign: "center" }}>
            <h1>TODOアプリケーション</h1>
            {currentUser && (
                <div>
                    <h2>ログインしています。</h2>
                    <button onClick={handleLogoutButton}>ログアウト</button>
                </div>
            )}
            {!currentUser && (
                <div style={{textAlign: "center"}}>
                    <h2>ログインしていません。</h2>
                    <button onClick={handleLoginButton}>ログイン</button>
                </div>
            )}
        </div>
    );
}

下記の様にログインボタンのみ表示されます。
ログインボタンを押すとGoogleアカウントの選択画面にリダイレクトされます。 Googleアカウントでログインすると「ログインしています。」の表示と共にログアウトボタンの表示のあるページにリダイレクトされれば実装は完了です。

nextjs-10-3

Lesson 10 Chapter 2
TODO登録画面の作成

はじめに

nextjs-10-4 本チャプターでセットアップしていく部分

本チャプターではCloud FirestoreにTODOを登録する処理を作成していきます。 作成していく流れは下記の様になります。

  • 1.TODOを登録する画面を作成する。
  • 2.1で作成した画面の入力した文字が登録ボタンをクリックすることでCloud Firestoreのコレクションにドキュメントとして挿入する処理を作成する。
  • 3.実際にCloud Firestore管理画面を確認して動作確認する。
  • 4.空文字を登録しようとするとエラーになる処理を作成する。
  • それでは作成していきます。

    TODO登録画面の作成

    TODOを登録する画面を作成していきます。ログイン処理を実装しているindex.jsxのreturn文の中を下記の様に変更します。

    index.jsx
       return (
        <>
            <h2 style={{textAlign: "center"}}>TODOアプリケーション</h2>
            {currentUser && (
                <>
                    <div style={{ textAlign: "right", marginRight: "40px" }}>
                        <h3>ログイン中</h3>
                        <button onClick={handleLogoutButton}>ログアウト</button>
                    </div>
                    <div style={{display: "flex", justifyContent: "center" }}>
                        <input />
                        <button>登録</button>
                    </div>
                </>
            )}
            {!currentUser && (
                <div style={{textAlign: "center"}}>
                    <h2>ログインしていません。</h2>
                    <button onClick={handleLoginButton}>ログイン</button>
                </div>
            )}
        </>
    );
    
    nextjs-10-5

    テキスト入力するinput要素と登録ボタンが配置できました。

    入力した文字を登録する処理を実装する

    以下の様に記述し、TODO登録処理を作成します。

    index.jsx
    import { addDoc, collection } from "firebase/firestore";
    import { useState } from "react";
    import { useAuth } from "../components/AuthContext";
    import { db } from "../firebase";
    
    export default function Index() {
        const { currentUser, login, logout } = useAuth();
        const [input, setInput] = useState('');
        const [changeFlg, setChangeFlg] = useState(true);
    
        const handleLoginButton = () => {
            login();
        };
    
        const handleLogoutButton = () => {
            logout();
        };
    
        const addTodo = async() => {
            await addDoc(collection(db, 'todos'),{ todo:input });
            setInput('');
            setChangeFlg(prevState=> !prevState)
        }
    
        return (
          <>
              <h2 style={{textAlign: "center"}}>TODOアプリケーション</h2>
              {currentUser && (
                  <>
                      <div style={{ textAlign: "right", marginRight: "40px" }}>
                          <h3>ログイン中</h3>
                          <button onClick={handleLogoutButton}>ログアウト</button>
                      </div>
                      <div style={{display: "flex", justifyContent: "center" }}>
                          <input value={input} onChange={(e) => {setInput(e.target.value)}}/>
                          <button onClick={addTodo}>登録</button>
                      </div>
                  </>
              )}
              {!currentUser && (
                  <div style={{textAlign: "center"}}>>
                      <h2>ログインしていません。</h2>
                      <button onClick={handleLoginButton}>ログイン</button>
                  </div>
              )}
          </>
      );
    }

    上記のコードの解説をしていきます。

    タスクを追加するinputイベントを作成

    タスクを追加するためには、input要素に入力されたテキストを取得しないといけません。
    そのため、useStateでinputの入力テキストを管理しています。

    const [input, setInput] = useState('');

    入力したデータを、input要素のonChangeメソッドでsetInputを使用してstateにセットします。

    <input value={input} onChange={(e) => {setInput(e.target.value)}}/>

    これで、input要素に入力されたテキストを取得・管理することができました。

    タスクを追加するクリックイベントを作成

    次に入力テキストをCloud Firestoreに登録するイベントを作成します。

    const addTodo = () => {
          addDoc(collection(db, 'todos'),{ todo:input });
          setInput('');
          setChangeFlg(prevState=> !prevState)
    }

    addDocメソッドでの第一引数でimportしたdb変数とコレクション名を指定し、第二引数で追加するフィールドとデータを指定します。 次に後ほど実装するTODOを一覧取得するフラグを切り替えます。

    <button onClick={addTodo}>登録</button>

    最後に上記の様にaddTodoクリックイベントを登録ボタンに設定して実装完了です。

    動作確認

    それでは開発サーバを起動して動作確認していきます。 TODOを入力し、todosコレクションにデータが追加されたら動作確認は完了です。

    TODOの登録処理が実装できました。次のチャプターではTODOの一覧表示、更新、削除を実装していきます。

    Lesson 10 Chapter 3
    TODO一覧画面の作成

    はじめに

    nextjs-10-6 本チャプターで実装していく部分

    本チャプターでは図で示した様に下記項目を実装していきます。

  • 登録したTODOを一覧表示させる。
  • 登録したTODOを編集するして更新する。
  • 登録したTODOを削除する。
  • それでは上記項目を作成していくにあたってまず画面を作成していきましょう。

    ビューの作成

    nextjs-10-7

    上記の様な画面を作成していきます。それではpages/index.jsxのreturn文を下記の様に記述編集します。

    pages/index.jsx
        return (
            <>
                <h2 style={{ textAlign: "center" }}>TODOアプリケーション</h2>
                {currentUser && (
                    <>
                        <div style={{ textAlign: "right", marginRight: "40px" }}>
                            <h3>ログイン中</h3>
                            <button onClick={handleLogoutButton}>ログアウト</button>
                        </div>
                        <div style={{ display: "flex", justifyContent: "center" }}>
                            <input
                                value={input}
                                onChange={(e) => {
                                    setInput(e.target.value);
                                }}
                            />
                            <button onClick={addTodo}>登録</button>
                        </div>
                        <h2 style={{ textAlign: "center", borderBottom: "solid" }}>
                            TODO一覧
                        </h2>
                        <ul
                            style={{
                                marginLeft: "620px",
                                display: "inline-block",
                                marginTop: "5px",
                            }}
    >
                            <div style={{ display: "flex", marginBottom: "10px" }}>
                                <li style={{ fontSize: "20px" }}>TODO一覧表示</li>
                                <input style={{ marginLeft: "10px" }}></input>
                                <button>更新</button>
                                <button>削除</button>
                            </div>
                        </ul>
                    </>
                )}
                {!currentUser && (
                    <div style={{textAlign: "center"}}>
                        <h2>ログインしていません。</h2>
                        <button onClick={handleLoginButton}>ログイン</button>
                    </div>
                )}
            </>
        );
    }

    完成画面の様な画面ができましたでしょうか。それでは一つずつ処理を実装していきます。

    TODO一覧表示機能の実装

    nextjs-10-8

    それでは上記の画面のListタグのアイテムに実際にコレクションからデータを取得して表示させてみましょう。 ここでFIrebase連携確認で使用したコードを流用して実装します。 下記の様にコードを記述することで一覧表示機能を実装できます。

    pages/index.jsx
    import { addDoc, collection, getDocs } from "firebase/firestore";
    import { useEffect, useState } from "react";
    import { useAuth } from "../components/AuthContext";
    import { db } from "../firebase";
    
    export default function Index() {
        const { currentUser, login, logout } = useAuth();
        const [input, setInput] = useState("");
        const [changeFlg, setChangeFlg] = useState(true);
        const [todos, setTodos] = useState([{ id: "", todo: "" }]);
    
    --省略--
    
        useEffect(() => {
            const firebase = async () => {
                try {
                    const col = collection(db, "todos");
                    const querySnapshot = await getDocs(col);
                    setTodos(
                        querySnapshot.docs.map((doc) => ({
                            id: doc.id,
                            todo: doc.data().todo,
                        }))
                    );
                } catch (error) {
                    console.log(error);
                }
            };
            firebase();
        }, [changeFlg]);    
    
        return (
    
    --省略--
    
                <h2 style={{ textAlign: "center", borderBottom: "solid" }}>
                    TODO一覧
                </h2>
                <ul
                    style={{
                        marginLeft: "620px",
                        display: "inline-block",
                        marginTop: "5px",
                    }}
                    >
                    {todos.map((todo) => {
                      return (
                          <div key={todo.id} tyle={{ display: "flex", marginBottom: "10px" }}>
                              <li style={{fontSize: "20px"}}>{todo.todo} </li>
                              <input style={{ marginLeft: "10px" }}> </input>
                              <button>更新 </button>
                              <button>削除 </button>
                          </div>
                      )
                    })}
                </ul>
            </>
        )}
    
    --省略--
    
    

    コードの解説をしていきます。

    useEffectでtodoの最新化

       useEffect(() => {
        const firebase = async () => {
            try {
                const col = collection(db, "todos");
                const querySnapshot = await getDocs(col);
                setTodos(
                    querySnapshot.docs.map((doc) => ({
                        id: doc.id,
                        todo: doc.data().todo,
                    }))
                );
            } catch (error) {
                console.log(error);
            }
        };
        firebase();
    }, [changeFlg]);  

    useEffectで初回レンダリング時と第二引数の登録時にセットしたChangeFlgの値が変わるたびにtodosコレクションから値を取得して更新します。

    todosのmapメソッドで展開

    {todos.map((todo) => {
      return (
          <div key={todo.id} tyle={{ display: "flex", marginBottom: "10px" }}>
              <li style={{fontSize: "20px"}}>{todo.todo} </li>
              <input style={{ marginLeft: "10px" }}> </input>
              <button>更新 </button>
              <button>削除 </button>
          </div>
      )
    })}

    useEffectで最新化したtodosをmapメソッドで展開して表示します。 これで一覧表示の実装が完了しました。次のチャプターではTODOの更新機能を実装していきます。

    TODO更新機能の実装

    それでは更新機能としてlistアイテムのinput欄に文字を入力して更新ボタンを押すことでTODOを更新できる機能を実装します。 実装は下記のコードで行います。

    pages/index.jsx
    import { addDoc, collection, doc, getDocs, setDoc } from "firebase/firestore";
    import { useEffect, useState } from "react";
    import { useAuth } from "../components/AuthContext";
    import { db } from "../firebase";
    
    export default function Index() {
        const { currentUser, login, logout } = useAuth();
        const [input, setInput] = useState("");
        const [changeFlg, setChangeFlg] = useState(true);
        const [todos, setTodos] = useState([{ id: "", todo: "" }]);
        const [updateTodo, setUpdateTodo] = useState([{ id: "", todo: "" }]);
                            
    --省略--
                          
        const editTodo = async (id) => {
          await setDoc(doc(db,"todos",id),{todo: updateTodo});
          setChangeFlg(prevState=> !prevState)
      }
                        
    --省略--
                      
      {todos.map((todo) => {
        return (
            <div key={todo.id} style={{ display: "flex", marginBottom: "10px" }}>
                <li style={{fontSize: "20px"}}>{todo.todo}</li>
                <input value={updateTodo.todo}
                    style={{ marginLeft: "10px" }} 
                    onChange={(e) => {
                        setUpdateTodo(e.target.value);
                }}></input>
                <button onClick ={() => {editTodo(todo.id)}}>更新</button>
                <button>削除</button>
            </div>
        )
    })}
    

    useStateのupdateTodo変数で更新するTODOの内容を管理します。 更新ボタンがクリックされたタイミングで該当listアイテムのidを引数としてeditTodo関数を実行します。
    editTodo関数ではidでマッチングしたtodoをupdateTodoで上書きます。
    その後ChangeFlgを切り替えてUseEffectを発火させます。

    TODOの削除処理

    登録、更新と同様の流れでTODOの削除を行います。 以下のコードを記述し、TODO削除機能を実装します。

    pages/index.jsx
    --省略--
        
        const deleteTodo = async (id) => {
          await deleteDoc(doc(db,"todos",id));
          setChangeFlg(prevState=> !prevState)
      }
                        
    --省略--
                      
      {todos.map((todo) => {
        return (
            <div key={todo.id} style={{ display: "flex", marginBottom: "10px" }}>
                <li style={{fontSize: "20px"}}>{todo.todo}</li>
                <input value={updateTodo.todo}
                    style={{ marginLeft: "10px" }} 
                    onChange={(e) => {
                        setUpdateTodo(e.target.value);
                }}></input>
                <button onClick ={() => {editTodo(todo.id)}}>更新</button>
                <button onClick ={() => {deleteTodo(todo.id)}}>削除</button>
            </div>
        )
    })}

    更新の時と同様に 削除ボタンがクリックされたタイミングで該当listアイテムのidを引数としてdeleteTodo関数を実行します。
    deleteTodo関数ではidでマッチングしたtodoを削除します。
    その後ChangeFlgを切り替えてUseEffectを発火させます。

    実装の完了

    これで一連の処理の実装が終わりました。 次のレッスンでログを出力するロギング周りの設定を行なっていきます。