Lesson 10

ReactとFirebaseでTODOアプリの作成 - 画面実装

Lesson 10 Chapter 1
アプリ画面の設計

アプリ画面のコンポーネント化

画面実装に先立って、このアプリでの画面のコンポーネント化の設計について説明します。レッスン9で説明したように、アプリには3つの画面表示がありますが、画面あるいは画面表示の一部は以下に示すようにコンポーネント化します。

2023-03-05-09-07-05.png サインイン画面

2023-03-05-16-28-29.png TODO登録・編集画面

2023-03-05-16-28-50.png TODO一覧画面

サインイン画面をSignIn.jsx、サインイン後の画面をToDo.jsxに記述します。また、サインイン後の画面において、TODO登録・編集用のコンポーネントをEdit.jsx、TODOの一覧表示のコンポーネントをList.jsxに記述します。このように画面表示として、2つのページ用と2つのコンポーネント用のファイルを作成します。なお、各コンポーネントに適用したスタイルは以下に掲載するstyles.jsになります。作成してsrcファルダに保存しましょう。アプリの作成が完了したら、良い見栄えになるよう各自でカスタマイズしてみてください。

styles.js
const title = {
  fontSize: "100px",
  color: "red"
};
                  
const toDoStyle = {
  padding: "10px" ,
  textAlign: "left",
  margin:"0 auto",
  display: "flex",
  justifyContent: "space-between"
};
                  
const listStyle = {
                     
};
                  
const editButton = {
  display: "inlineBlock"
};
                  
                  
const textbox = {
  width: "80%"
};
                  
const header = {
  display: "flex",
  justifyContent: "space-between"
};
                  
const modeChangeButton = {
  height: "50px",
};
                  
const logoutButton = {
  height: "50px"
};
                  
const additionButton= {
  height: "50px",
  backgroundColor: "red",
  color:"white",
  fontSize: "20px"
};
                  
export {title, toDoStyle, listStyle, editButton, textbox, header ,modeChangeButton, logoutButton, additionButton};

ルーティングの設定

アプリの画面遷移をReact Routerを使用して設定します。App.jsxに以下のように記述します。

App.jsx
import './App.css';
import { memo } from "react";
import { SignIn } from './pages/SignIn'
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import { ToDo } from './pages/ToDo';
import { List } from './components/List';
import { Edit } from './components/Edit';
                      
const App = memo(() => {
                        
  return (
    <div className="App">
      <BrowserRouter>
        <Routes>
          <Route path="/" element={ <Navigate to="/signin" replace/>} />
          <Route path="/signin" element={ <SignIn />} />
          <Route path="/to_do" element={ <ToDo />} >
            <Route index element={ <List /> } />
            <Route path="edit" element={ <Edit /> } />
          </Route>
          <Route path="*" element={<Navigate to='/signin' replace />} />
        </Routes>
      </BrowserRouter>
    </div>
  );
                      
});
                      
export { App };

コード先頭でimportしている画面表示用のコンポーネントファイルは現時点で作成してませんが、これから作成していきます。このアプリのURLの"/to_do"あるいは"/to_do/edit"以外にアクセスすると必ずサインイン画面に飛ぶようになっています(element={<Navigate to='/signin' replace />}がサインイン画面へのリダイレクトを意味します。)。また、サインインしていない状態で"/to_do"あるいは"/to_do/edit"にアクセスした場合の処置はここでは記述されていませんが、サインイン後の画面のコンポーネントファイルでサインイン画面にリダイレクトするように設計します。ListコンポーネントとEditコンポーネントはToDoコンポーネントの内部表示となるので、RouteコンポーネントをToDoコンポーネントを持つRouteコンポーネントにネストします。Appコンポーネントについて、レンダリングを最適化のためのメモ化を忘れないようにしましょう。

Lesson 10 Chapter 2
サインイン画面の実装(SignIn.jsx)

ビューの作成

サインイン画面を表示するコンポーネントSignIn.jsxをpagesフォルダに作成します。以下に内容を掲載するので、画面表示を想像しながら作成していきましょう。

SignIn.jsx
import { memo } from "react";
import { title } from "../styles";
                      
const SignIn = memo(() => {
  return (
    <>
      <div>
        <h1 style={title}>Simple To Do App.</h1>
       </div>
      <div>
        <button onClick={}>
          <p>サインイン</p>
        </button>
      </div>
    </>
  );
});
                      
export { SignIn };

"Simple To Do App."というアプリタイトルとサインインボタンを表示するシンプルな画面になります。サインインボタンのイベントハンドラonClickにはこの後作成するサインイン用のカスタムフックを設定します。SignIn関数はレンダリングを最適化のためにメモ化しておきましょう。

サインイン処理の実装

サインイン処理を実装します。最初に複数の画面から参照される認証情報をグローバルなStateとして管理できるように以下のUserInfoProvider.jsxを作成します。作成したら、contextフォルダに保存しましょう。

UserInfoProvider.jsx
import { useState, createContext } from "react";

const UserInfoContext = createContext({});
                      
const UserInfoProvider = props => {

  const { children } = props;
                      
  const [user, setUser] = useState(null);
                      
  return (
    <UserInfoContext.Provider value = {{user, setUser}}>
      {children}
    </UserInfoContext.Provider>
  );

}
                      
export { UserInfoContext, UserInfoProvider };

認証情報を格納する"user"とuserを更新する関数として"setUser"を定義しています。この2つをグローバルStateとして、useContextで取得できるよう設定します。また、UserInfoContextとUserInfoProviderはコンポーネントファイルやこの後作成する認証用のカスタムフックで使用するので最後にexportしています。

次に、認証用の機能を扱うカスタムフックを作成します。以下に示すuseAuthorization.jsを作成し、hooksフォルダに保存しましょう。

useAuthorization.js
import { auth, provider } from "../firebase"
import { signInWithPopup } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useContext } from "react"
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useAuthorization = () => {
                      
  const {user, setUser} = useContext(UserInfoContext);
  const navigate = useNavigate();
  const location = useLocation();
                      
  const signInWithGoogleAccount = async () => {
    const result = await signInWithPopup(auth, provider);
    setUser(result.user.auth.currentUser);
    navigate("/to_do");
  }
                      
  return {user,  signInWithGoogleAccount }
                      
} 
                      
export { useAuthorization };

useAuthorization.jsでは、サインイン処理を実行するsignInWithGoogleAccount関数を定義しています。signInWithGoogleAccount関数の定義部は以下になります。

const signInWithGoogleAccount = async () => {
  const result = await signInWithPopup(auth, provider);
  setUser(result.user.auth.currentUser);
  navigate("/to_do");
}

非同期処理を実行するので、asyncを付与した関数として定義しています。関数内部では最初に非同期処理"signInWithPopup(auth, provider)"を実行します。この関数が実行されると、サインインするグーグルアカウント選択用のポップアップを表示し、選択するとそのアカウントでサインインを実行します。関数の第一引数にはfirebase.jsで取得した認証情報を、第二引数にはGoogleプロバイダオブジェクトを与えます。戻り値はサインインしたグーグルアカウント情報になります。signInWithPopupは"firebase/auth"からimportします。"signInWithPopup(auth, provider)"の実行が完了すると、setUser関数でuserにサインインしているユーザー情報である"result.user.auth.currentUser"をセットします。最後に、"navigate("/to_do");"で"/to_do"のURLに移動させる処理を実行します。navigateはreact-router-domのライブラリの関数です。userとsignInWithGoogleAccount関数をコンポーネントファイルで使用するので、useAuthorization関数の戻り値に設定します。最後にuseAuthorization関数をexportします。

useAuthorization.jsにて作成したサインイン機能をサインイン画面に設定します。以下に習って、サインインボタンにsignInWithGoogleAccount関数を設定しましょう。

SignIn.jsx
import { memo } from "react";
import { title } from "../styles";
import { useAuthorization } from "../hooks/useAuthorization";
                      
const SignIn = memo(() => {
  const { signInWithGoogleAccount } = useAuthorization();
                      
  return (
    <>
      <div>
        <h1 style={title}>Simple To Do App.</h1>
       </div>
      <div>
        <button onClick={signInWithGoogleAccount}>
          <p>サインイン</p>
        </button>
      </div>
    </>
  );
});
                      
export { SignIn };

サインインアウト処理の実装

useAuthorization.jsにサインアウト処理を追加します。signOut関数を以下に習って追加しましょう。

useAuthorization.js
import { auth, provider } from "../firebase"
import { signInWithPopup, signOut as signOutFromFirebase } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useContext } from "react"
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useAuthorization = () => {
                      
  const {user, setUser} = useContext(UserInfoContext);
  const navigate = useNavigate();
                      
  const signOut = async () =>{
    await signOutFromFirebase(auth);
    setUser(null);
  }
                      
  const signInWithGoogleAccount = async () => {
    const result = await signInWithPopup(auth, provider);
    setUser(result.user.auth.currentUser);
    navigate("/to_do");
  }
                      
  return {user,  signInWithGoogleAccount, signOut }
                      
} 
                      
export { useAuthorization }

signOut関数の定義部は以下になります。

const signOut = async () =>{
  await signOutFromFirebase(auth);
  setUser(null);
}

signOut関数も内部で非同期処理を実行します。処理の最初に非同期処理"signOutFromFirebase(auth)"を実行します。signOutFromFirebase関数は元々"signOut"という名前の関数ですが、同名の関数をこのファイルで定義するので、"signOut as signOutFromFirebase"の記述により名前を変更して"firebase/auth"からimportしています。この関数でサインアウト処理を実行できます。最後に、"setUser(null);"でuserをnullで上書きします。signOut関数をuseAuthorization関数の戻り値に追加するのを忘れないようにしましょう。

このアプリではサインイン状態でサインイン画面を表示したらサインアウトさせる仕様にしています。useAuthorization.jsにて作成したサインアウト機能をサインイン画面に設定します。以下のコメント部を参考に、サインイン画面が表示されたらサインアウト機能が実行されるように記述しましょう。

SignIn.jsx
import { useEffect, memo } from "react";
import { title } from "../styles";
import { useAuthorization } from "../hooks/useAuthorization";
                      
const SignIn = memo(() => {
  const { signInWithGoogleAccount, signOut } = useAuthorization();
                      
  useEffect(() => {
    signOut(); //ここでサインアウトを実行
  }, []);
                      
  return (
    <>
      <div>
        <h1 style={title}>Simple To Do App.</h1>
      </div>
      <div>
        <button onClick={signInWithGoogleAccount}>
          <p>サインイン</p>
        </button>
      </div>
    </>
  );
});
                      
export { SignIn };

以上で、SignIn.jsxのコンポーネントの作成は完了です。

エラー処理の実装

useAuthorization.jsにて定義したサインインおよびサインアウト処理は、エラーになることも想定されます。処理が失敗した場合のエラー処理を追加しておきましょう。サインインおよびサインアウト処理を実行する関数について、try{ . . . }catch{ . . . }の構文を使用して非同期処理の成功と失敗の場合に分けて処理を記述しましょう。

useAuthorization.js
import { auth, provider } from "../firebase"
import { signInWithPopup, signOut as signOutFromFirebase } from "firebase/auth";
import { useNavigate } from "react-router-dom";
import { useContext } from "react"
import { UserInfoContext } from "../context/UserInfoProvider";
                      
onst useAuthorization = () => {
                      
  const {user, setUser} = useContext(UserInfoContext);
  const navigate = useNavigate();
                      
  const signOut = async () =>{
    try{
      await signOutFromFirebase(auth);
      setUser(null);
    } catch {
      alert("Failed to sign-out.");
    }
  }
                      
  const signInWithGoogleAccount = async () => {
    try{
      const result = await signInWithPopup(auth, provider);
      setUser(result.user.auth.currentUser);
      navigate("/to_do");
    } catch {
      alert("Failed to sign-in.");
    }
  }
                      
  return {user,  signInWithGoogleAccount, signOut }
                      
} 
                      
export { useAuthorization }

エラー処理としては、サインインが失敗すると"Failed to sign-in."、サインアウトが失敗すると"Failed to sign-out."のメッセージを表示するアラートを設定しています。

Lesson 10 Chapter 3
TODO画面の実装(ToDo.jsx)

ビューの作成

TODOの登録・編集画面と一覧画面のレイアウトとなるコンポーネント"ToDo.jsx"をpagesフォルダに作成します。以下に内容を掲載するので、画面表示を想像しながら作成していきましょう。

ToDo.jsx
import { useState, memo } from "react";
import { Outlet, Navigate } from 'react-router-dom';
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
import { header, modeChangeButton, logoutButton } from "../styles";
                      
const ToDo = memo(() => {
  const [editing, setEditing] = useState(false);
  const { user } = useContext(UserInfoContext);
                      
  return (
    !user ? (
      <Navigate to="/signin" replace />
    ) : (
      <div>
        <div style={header}>
          <button onClick={() => {}} style={modeChangeButton}>
            {editing ? "表示" : "編集"}
          </button>
          <button onClick={() => {}} style={logoutButton}>
            サインアウト
          </button>
        </div>
        <fieldset>
          <legend>{`${user.displayName}さんのTo Do`}</legend>
          <Outlet />
        </fieldset>
      </div>
    )
  );
});
                      
export { ToDo };

関数の最初でState"editing"を定義しています。editingはこの後説明する画面表示の切り替えに使用するStateになります。その下では、ログインしているユーザー情報を持つグローバルState"user"を取得しています。return文を見ると、"!user"の評価により表示内容を分岐しています。userはユーザー情報なので、"!user"がtrueの場合というのはユーザーがサインインしていない場合を意味します。その場合、<Navigate to='/signin' replace />でサインイン画面にリダイレクトさせます。ユーザー情報がある場合は":( . . . )"の . . . の内容を表示します。全体を囲うdivタグの中にあるdivタグはページのヘッダの部分になります。ヘッダには2つのボタンを配置します。一つは表示切替用のボタン、もう一つはサインアウト用のボタンになります。2つのボタンのイベントハンドラonClickはこの後定義します。その下のfieldsetタグの内部にTODOの情報を表示します。legendタグの"user.displayName"はサインインしているユーザーの名前になるので、"○○さんのTo Do"の"○○"の部分がユーザーによって変わるようになっています。<Outlet />にはURLによりEdit.jsxかList.jsxいずれかのコンポーネントが表示されます。ToDo関数もレンダリングを最適化のためにメモ化を忘れないようにしましょう。

サインアウト処理の実装

ToDo.jsxにサインアウトボタンのonClickの実行関数であるtoSignInPage関数を追加します。以下にtoSignInPage関数を追加したToDo.jsxを掲載します。コメントの場所が追加したコードになります。

ToDo.jsx
import { useState, memo } from "react";
import { Outlet, useNavigate, Navigate } from 'react-router-dom'; //useNavigateを追加
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
import { header, modeChangeButton, logoutButton } from "../styles";
                      
const ToDo = memo(() => {
  const [ editing, setEditing ] = useState(false);
  const { user } = useContext(UserInfoContext);
  const navigate = useNavigate();  //追加
                      
  const toSignInPage = () => {
    navigate("/signin");
  };  //追加
                      
  return (
    !user ?
    <Navigate to="/signin" replace/> :
    (
       <div>
        <div style={header}>
          <button onClick={setEditing(!editing)} style={modeChangeButton}>
            {editing ? "表示" : "編集"}
          </button>
          <button onClick={toSignInPage} style={logoutButton}>
            サインアウト
          </button>
        </div>
        <fieldset>
          <legend>{user.displayName}さんのTo Do</legend>
          <Outlet />
        </fieldset>
      </div>
    )
  );
});
                      
export { ToDo };

toSignInPage関数の処理部は"navigate("/signin")"となっています。これより、クリックするとサインイン画面に移動するボタンになります。サインイン画面に戻ると強制的にサインアウトするので、実質サインアウトと同じ機能になります。

画面切り替え処理の実装

ToDo.jsxに表示切替用ボタンのonClickの実行関数であるchangeMode関数を追加します。以下にchangeMode関数を追加したToDo.jsxを掲載します。コメントの場所が追加したコードになります。

ToDo.jsx
import { useState, memo } from "react";
import { Outlet, useNavigate, Navigate } from 'react-router-dom';
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
import { header, modeChangeButton, logoutButton } from "../styles";
                      
const ToDo = memo(() => {
  const [editing, setEditing] = useState(false);
  const { user } = useContext(UserInfoContext);
  const navigate = useNavigate();
                      
  const changeMode = () => {
    navigate(editing ? "/to_do" : "edit");
    setEditing(!editing);
  };  //追加
                      
  const toSignInPage = () => {
    navigate("/signin");
  };
                      
  return !user ? (
    <Navigate to="/signin" replace />
  ) : (
    <div>
      <div style={header}>
        <button onClick={changeMode} style={modeChangeButton}>
          {editing ? "表示" : "編集"}
        </button>
        <button onClick={toSignInPage} style={logoutButton}>
          サインアウト
        </button>
      </div>
      <fieldset>
        <legend>{user.displayName}さんのTo Do</legend>
        <Outlet />
      </fieldset>
    </div>
   );
});
                      
export { ToDo };

changeMode関数の処理部の一行目は"navigate(editing?"/to_do":"edit");"となっています。これによりeditingがtrueであると"/to_do"、falseであると"/to_do/edit"にURLが切り変わります。最後に"setEditing(!editing);"でeditingの状態を反転させます。以上で、ToDo.jsxコンポーネントの作成は完了です。

Lesson 10 Chapter 4
TODO登録・編集画面の作成(Edit.jsx)

ビューの作成

ToDo登録・編集画面を表示するコンポーネント"Edit.jsx"をcomponentsフォルダに作成します。内容は以下のようになります。

Edit.jsx
import { memo } from "react";
import { toDoStyle, listStyle, editButton, textbox, additionButton } from "../styles";
                      
const Edit = memo(() => {
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) => (
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                {editingId === document.id ? (
                  <input type="text" defaultValue={document.task} onChange={} style={textbox} />
                ) : (
                  <label>{document.task}</label>
                )}
                <button onClick={} style={editButton}>
                  {editingId === document.id ? "完了" : "編集"}
                </button>
              </div>
            </li>
          )
        ))}
      </ul>
      {additingToDo && <input type="text" onChange={} style={textbox} />}
      <button onClick={} style={additingToDo ? {} : additionButton}>
        {additingToDo ? "完了" : "新規登録"}
      </button>
    </div>
  );
});
                      
export { Edit };

現段階では機能は未作成なので、表示の枠組のみ記述しています。表示部の構成は以下になります。

<div style = {listStyle}>
  <ul>
    {loadedCollection.map((document) => (
      document.task&& (登録されたTODOを表示する処理)                    
    ))
    }
  </ul>
  (TODOを登録する処理)
</div>

"loadedCollection"は後ほど作成しますが、サブコレクション"TODO"のドキュメントの内容を要素に持つ配列になります(本アプリのサブコレクションについてはレッスン9をご確認ください)。loadedCollectionのmap関数で配列要素を表示する処理をしています。map関数の処理部は、"document.task&&(登録されたTODOを表示する処理)"となっています。document.taskは登録されたTODOの内容になります。よって配列要素の各ドキュメントにTODOが登録されていれば、登録されたTODOを表示する処理を実行するという内容になります。登録されたTODOを表示する処理は以下になります。

 <li key={document.id}>
  <div style={toDoStyle}>
    {editingId === document.id ? (
      <input type="text" defaultValue={document.task} onChange={} style={textbox} />
    ) : (
      <label>{document.task}</label>
    )}
    <button onClick={} style={editButton}>
      {editingId === document.id ? "完了" : "編集"}
    </button>
  </div>
</li>

TODOはliタグを使って箇条書きで表示します。divタグ内の2行の内、上の行では編集時はTODO編集用のテキストボックス、非編集時はTODOの内容を表示するよう記述しています。下の行ではTODOの編集状態のONOFF用のボタンを定義しています。詳細は後ほど説明します。TODOを登録する処理は以下になります。

{additingToDo && <input type="text" onChange={} style={textbox} />}
<button onClick={} style={additingToDo ? {} : additionButton}>
  {additingToDo ? "完了" : "新規登録"}
</button>

"additingToDo"はTODO登録中であればtrueとなるStateです。TODO登録中であれば&&の右で定義されているテキストボックスを表示します。表示されたテキストボックスに新規TODOを入力します。additingToDoについても後ほど定義します。下の行で定義しているボタンはTODOの登録をONOFFする機能になります。機能の実装に使用する関数はこの後定義します。

TODO取得処理の実装

TODO取得処理を実装します。TODOに関する処理をまとめたカスタムフックuseToDo.jsを作成しhooksフォルダに保存しましょう。TODO取得処理はgetToDos関数として作成します。以下を参考にuseToDo.jsにTODO取得処理を記述しましょう。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos = () => {
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
                                            
  const getToDos = async () => {
    const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
    const snapShot = await getDocs(data);
    setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
    setIsUpdated(false);
  };
                                                     
  return { loadedCollection, isUpdated, setIsUpdated, getToDos };
};
                      
export { useToDos };

useToDosの最初に、グローバルState"user"を取得し、"loadedCollection"と"isUpdated"というStateを定義しています。loadedCollectionは先ほど説明したようにサブコレクション"TODO"のドキュメントの内容を要素に持つ配列であり、isUpdatedはTODOが更新されたかどうかを表す真偽値になります。その後にTODO取得処理を実行するgetToDos関数を定義しています。処理内容は以下になります。

const getToDos = async () => {
  const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
  const snapShot = await getDocs(data);
  setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
  setIsUpdated(false);
};

関数内の1行目の処理でサブコレクション"TODO"をドキュメントを作成日順にソートして取得するクエリを作成しています。2行目の処理で1行目のクエリを実行し、結果を"snapShot"に格納します。こちらは非同期処理となります。3行目では、ドキュメントのデータとドキュメントIDを有するオブジェクトを要素とする配列を作成し、loadedCollectionに上書きしています。4行目では、isUpdatedをfalseに設定しています。

useToDo.jsにTODO取得処理を記述できたら、Edit.jsxに追加しましょう。以下に習ってEdit.jsxを書き換えます。

Edit.jsx
import { useEffect, memo } from "react"; //useEffectを追加
import { useToDos } from "../hooks/useToDos";    //追加
import { toDoStyle, listStyle, editButton, textbox, additionButton } from "../styles";
                      
const Edit = memo(() => {
  const { loadedCollection, isUpdated, getToDos } = useToDos();    //追加
                        
  useEffect(() => {
    getToDos();
  }, [isUpdated]);    //追加
                        
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) =>
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                {editingId === document.id ? (
                  <input
                    type="text"
                    defaultValue={document.task}
                    onChange={() => {}}
                    style={textbox}
                  />
                ) : (
                  <label>{document.task}</label>
                )}
                <button onClick={() => {}} style={editButton}>
                  {editingId === document.id ? "完了" : "編集"}
                </button>
              </div>
            </li>
          )
        )}
      </ul>
      {additingToDo && <input type="text" onChange={() => {}} style={textbox} />}
      <button onClick={() => {}} style={additingToDo ? {} : additionButton}>
        {additingToDo ? "完了" : "新規登録"}
      </button>
    </div>
  );
});
                      
export { Edit };

コメントが付与された部分が新たに追加した部分になります。関数コンポーネントの最初にuseToDoからloadedCollection, isUpdated, getToDosを取得しています。その後、useEffectを使用して、isUpdatedが変化した時にTODO取得処理を実行するよう記述しています。これにより、TODOの更新時にデータベースの内容の自動取得が可能となります。

TODO登録処理の実装

TODO登録処理を実装します。TODO登録処理はaddToDo関数として作成します。カスタムフックuseToDo.jsに以下を参考にしてTODO登録処理を追加しましょう。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                                            
const useToDos = () => {
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
                                            
  const getToDos = async () => {
  const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
  const snapShot = await getDocs(data);
  setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
  setIsUpdated(false);
  }
                                            
  const addToDo = async () => {
    if (additingToDo && addedToDo !== "") {
      await addDoc(collection(db, "users", user.uid, "TODO"), {
        task: addedToDo,
        created_date: serverTimestamp(),
        finished: false
      });
      setIsUpdated(true);
    } else {
      setAddedToDo("");
    }
    setAdditingToDo(!additingToDo);
  }
                                                     
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo };
}
                                            
export { useToDos };

"additingToDo"と"addedToDo"というStateを新たに追加しています。additingToDoは登録中かどうかを表す真偽値、addedToDoは登録するTODOの内容を表す文字列になります。TODO登録処理を実行するのは以下に示すaddToDo関数になります。

 const addToDo = async () => {
  if (additingToDo && addedToDo !== "") { //TODO登録中かつ登録するTODOの内容が入力されている
    await addDoc(collection(db, "users", user.uid, "TODO"), {
      task: addedToDo,
      created_date: serverTimestamp(),
      finished: false
    });
    setIsUpdated(true);
  } else {  //TODO登録中でないあるいは登録するTODOの内容が入力されていない
    setAddedToDo("");
  }
  setAdditingToDo(!additingToDo);
}

TODO登録中かつ登録するTODOの内容が入力されているならば、if文の中を実行します。実行する内容はサブコレクション"TODO"への新規ドキュメントの追加です。こちらは非同期処理となります。追加するドキュメントの内容は以下のオブジェクトになります。

{
  task:登録するTODOの内容, 
  created_date: サーバーのタイムスタンプ, 
  finished: false
}

新規ドキュメントを追加後、isUpdatedをtrueに更新します。TODO登録中でないあるいは登録するTODOの内容が入力されていない場合、else文の内容が実行されます。実行内容は空の文字列でaddedToDoを上書きすることになります。if文の結果によらず、最後にadditingToDoの状態を反転します。

useToDo.jsにTODO登録処理を記述できたら、Edit.jsxに追加しましょう。以下に習ってEdit.jsxを書き換えます。

Edit.jsx
import { useEffect, memo } from "react";
import { toDoStyle, listStyle, editButton, textbox, additionButton } from "../styles";
import { useToDos } from "../hooks/useToDos";
                                            
const Edit = memo(() => {
  const {
    loadedCollection,
    isUpdated,
    getToDos,
    additingToDo,
    setAddedToDo,
    addToDo
  } = useToDos(); //additingToDo, setAddedToDo, addToDoを追加
                      
  const onChangeAdditionalText = (event) => setAddedToDo(event.target.value); //追加
  const onClickAdd = () => addToDo(); //追加
                      
  useEffect(() => {
    getToDos();
  }, [isUpdated]);
                                            
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) => (
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                {editingId === document.id ?
                  <input
                    type="text"
                    defaultValue={document.task}
                    onChange={}
                    style={textbox}
                  />
                  :
                  <label>{document.task}</label>
                }
                <button onClick={} style={editButton}>
                  {editingId === document.id ? "完了" : "編集"}
                </button>
              </div>
            </li>
           )                    
        ))}
      </ul>
      {additingToDo &&
        <input
          type="text"
          onChange={(event) => onChangeAdditionalText(event)}
          style={textbox}
        />  {/*event=>onChangeAdditionalText(event)を追加*/}
      }
      <button
        onClick={onClickAdd}
        style={additingToDo ? {} : additionButton}
      >
        {additingToDo ? "完了" : "新規登録"}
      </button> {/*onClickAddを追加*/}
    </div>
  );                          
});
                                          
export { Edit };

コメントが付与された部分が新たに追加した部分になります。useToDoからadditingToDo, setAddedToDo, addToDoを追加で取得しています。その後、登録用TODOのテキストを取得するonChangeAdditionalText関数とTODOの登録を実行するonClickAdd関数を定義しています。これらの関数が設定されるのは以下の表示部になります。

{additingToDo &&
  <input
    type="text"
    onChange={(event) => onChangeAdditionalText(event)}
    style={textbox}
  />  {/*event=>onChangeAdditionalText(event)を追加*/}
}
<button
  onClick={onClickAdd}
  style={additingToDo ? {} : additionButton}
>
  {additingToDo ? "完了" : "新規登録"}
</button> {/*onClickAddを追加*/}

1行目のadditingToDoはTODO登録中かどうかを表すStateなので、TODO登録中であればテキストボックスを表示します。表示したテキストボックスにテキストが入力されると、onChangeAdditionalText関数が実行され、addedToDoに入力内容が格納されます。2行目のボタンはTODO登録中であれば"完了"、そうでなければ"新規登録"を表示し、クリックするとonClickAdd関数が実行されます。onClickAdd関数はTODO登録中でないときに実行すれば登録中の状態に切り替え、TODO登録中に実行すれば登録内容をデータベースに反映します。

TODO編集処理の実装

TODO編集処理を実装します。TODO編集処理はeditToDo関数として作成します。カスタムフックuseToDo.jsに以下を参考にしてTODO編集処理を追加しましょう。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp, setDoc, doc } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos =() => {
                      
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
  const [editingId, setEditingId] = useState("");
  const [editedToDo, setEditedToDo] = useState("");
                      
  const getToDos = async () => {
    const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
    const snapShot = await getDocs(data);
    setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
    setIsUpdated(false);
  }
                      
  const addToDo = async () => {
    if (additingToDo&&addedToDo !== "") {
      await addDoc(collection(db, "users", user.uid, "TODO"), {task:addedToDo, created_date: serverTimestamp(), finished: false});
      setIsUpdated(true);
    }else{
      setAddedToDo( "" );
    }
    setAdditingToDo(!additingToDo);
  }
                      
  const editToDo = async (id) => {
    if (id === editingId ) {
      await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
      setEditedToDo("");
      setIsUpdated(true);
      setEditingId("");
    } else {
      setEditingId(id);
    }
  }
                               
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo };
                      
}
                      
export { useToDos };

"editingId"と"editedToDo"というStateを新たに追加しています。editingIdは編集するTODOのドキュメントID、editedToDoは編集するTODOの内容を表す文字列になります。TODO編集処理を実行するのは以下に示すeditToDo関数になります。

const editToDo = async (id) => {
  if (id === editingId ) {
    await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
    setEditedToDo("");
    setIsUpdated(true);
    setEditingId("");
  } else {
    setEditingId(id);
  }
}

関数の引数"id"はデータベースのドキュメントIDになります。データベースのドキュメントIDと編集するTODOのドキュメントIDが一致すれば、if文の中を実行します。実行する内容はサブコレクション"TODO"の指定されたIDのドキュメントのtaskフィールドの上書きです。こちらは非同期処理となります。上書きが完了すると、editedToDoとeditingIdを空の文字列で上書きし、isUpdatedをtrueに更新します。データベースのドキュメントIDと編集するTODOのドキュメントIDが一致していなければ、else文の中を実行します。実行するのはeditingIdにデータベースのドキュメントIDを格納することになります。

useToDo.jsにTODO編集処理を記述できたら、Edit.jsxに追加しましょう。以下に習ってEdit.jsxを書き換えます。

Edit.jsx
import { useEffect, memo } from "react";
import { toDoStyle, listStyle, editButton, textbox, additionButton } from "../styles";
import { useToDos } from "../hooks/useToDos";
                      
const Edit = memo(() => {
  const { loadedCollection, isUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo } = useToDos();  //editingId, setEditedToDo, editToDoを追加
                      
  const onChangeAdditionalText = (event) => setAddedToDo(event.target.value);
  const onClickAdd = () => addToDo();
  const onChangeText = (event) => setEditedToDo(event.target.value);  //追加
  const onClickEdit = (id, task) => editToDo(id, task); //追加
                      
  useEffect(() => {
    getToDos();
  }, [isUpdated]);
                      
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) => (
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                {editingId === document.id ? (
                  <input type="text" defaultValue={document.task} onChange={event => onChangeText(event)} style={textbox} />
                ) : (
                  <label>{document.task}</label>  {/*event=>onChangeText(event)を追加*/}
                )}
                <button onClick={() => onClickEdit(document.id, document.task)} style={editButton}>
                  {editingId === document.id ? "完了" : "編集"}
                </button> {/*()=>onClickEdit(document.id, document.task)を追加*/}
              </div>
            </li>
          )
        ))}
      </ul>
      {additingToDo && <input type="text" onChange={event => onChangeAdditionalText(event)} style={textbox} />}
      <button onClick={onClickAdd} style={additingToDo ? {} : additionButton}>
        {additingToDo ? "完了" : "新規登録"}
      </button>
    </div>
  );
});
                      
export { Edit };

コメントが付与された部分が新たに追加した部分になります。useToDoからeditingId, setEditedToDo, editToDoを追加で取得しています。その後、編集用TODOのテキストを取得するonChangeText関数とTODOの編集を実行するonClickEdit関数の定義を追加しています。これらの関数が設定されるのは以下の表示部になります。

<ul>
  {loadedCollection.map((document) => (
    document.task && (
      <li key={document.id}>
        <div style={toDoStyle}>
          {editingId === document.id ? (
            <input type="text" defaultValue={document.task} onChange={event => onChangeText(event)} style={textbox} />
          ) : (
            <label>{document.task}</label>  {/*event=>onChangeText(event)を追加*/}
          )}
          <button onClick={() => onClickEdit(document.id, document.task)} style={editButton}>
            {editingId === document.id ? "完了" : "編集"}
          </button> {/*()=>onClickEdit(document.id, document.task)を追加*/}
        </div>
      </li>
    )
  ))}
</ul>

loadedCollectionの要素の中身について各々map関数の中を実行します。divタグの中を確認すると、1行目が三項演算子になっています。編集するドキュメントIDとmap関数の中で参照しているドキュメントのIDが等しければ編集用のテキストボックスを、そうでなければTODOの内容を表示するといった内容になっています。編集用のテキストボックスのonChangeイベントにイベントオブジェクトを引数に取るonChangeTex関数が設定されており、入力内容をeditedToDoに上書きします。その下のボタンは編集するドキュメントIDとmap関数の中で参照しているドキュメントのIDが等しければ"完了"、そうでなければ"編集"を表示し、クリックするとdocument.idとdocument.taskを引数としてonClickEdit関数が実行されます。onClickEdit関数はTODO編集中でないときに実行すれば編集中の状態に切り替え、TODO編集中に実行すれば編集内容をデータベースに反映します。

以上で、Edit.jsxコンポーネントの作成は完了です。

エラー処理の実装

useToDO.jsにて定義したgetToDos、addToDo、editToDoに対してもエラー処理を追加しておきましょう。try{ . . . }catch{ . . . }の構文を使用してエラー処理を実装したuseToDO.jsが以下になります。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp, setDoc, doc } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos =() => {
                      
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
  const [editingId, setEditingId] = useState("");
  const [editedToDo, setEditedToDo] = useState("");
                      
  const getToDos = async () => {
    try{
      const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
      const snapShot = await getDocs(data);
      setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
      setIsUpdated(false);
    }catch{
      alert("Failed to get ToDos.");
    }    
  }
                      
  const addToDo = async () => {
    if (additingToDo&&addedToDo !== "") {
      try{
        await addDoc(collection(db, "users", user.uid, "TODO"), {task:addedToDo, created_date: serverTimestamp(), finished: false});
        setIsUpdated(true);
      }catch{
        alert("Failed to add a ToDo.");
      } 
    }else{
      setAddedToDo( "" );
    }
    setAdditingToDo(!additingToDo);
  }
                      
  const editToDo = async (id) => {
    if (id === editingId ) {
      try{
        await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
        setEditedToDo("");
        setIsUpdated(true);
      }catch{
        alert("Failed to edit the ToDo.");
      } 
      setEditingId("");
    }else{
      setEditingId(id);
    }
  }
                               
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo };
                      
}
                      
export { useToDos };

エラー処理として、TODO取得に失敗すると"Failed to get ToDos."、TODO登録に失敗すると"Failed to add a ToDo."、TODO編集に失敗すると"Failed to edit the ToDo."のメッセージを表示するアラートを設定しています。

Lesson 10 Chapter 5
TODO一覧画面の作成(List.jsx)

ビューの作成

TODO一覧画面を表示するコンポーネント"List.jsx"をcomponentsフォルダに作成します。内容は以下のようになります。

List.jsx
import { useEffect, memo } from "react";
import { toDoStyle, listStyle, editButton } from "../styles";
import { useToDos } from "../hooks/useToDos";
                      
const List = memo(() => {
  const { isUpdated, loadedCollection, getToDos } = useToDos();
                      
  useEffect(() => {
    getToDos();
  }, [isUpdated]);
                      
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) => (
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                <label style={{ textDecoration: document.finished ? "line-through" : "none" }}>
                  {document.task}
                  <input type="checkbox" onChange={} checked={document.finished} />
                </label>
                <button onClick={} style={editButton}>
                  削除
                </button>
              </div>
            </li>
          )
        ))}
      </ul>
    </div>
  );
});
                      
export { List };

現段階までに作成した機能で表示の枠組を記述しています。TODOの内容の表示ロジックはEdit.jsxと同様です。TODOの表示部は以下になります。

<ul>
  {loadedCollection.map((document) => (
    document.task && (
      <li key={document.id}>
        <div style={toDoStyle}>
          <label style={{ textDecoration: document.finished ? "line-through" : "none" }}>
            {document.task}
            <input type="checkbox" onChange={} checked={document.finished} />
          </label>
          <button onClick={} style={editButton}>
            削除
          </button>
        </div>
      </li>
    )
  ))}
</ul>

Edit.jsxと同様に、TODOはliタグを使って箇条書きで表示します。liタグ中のdivタグ内はTODOの内容とチェックボックスを表示するlabel要素とTODOを削除するbutton要素からなります。TODOの実行完了を表す真偽値"document.finished"がtrueになると、TODOの内容に取り消し線が表示され、チェックボックスにチェックが入るように記述されています。チェックボックスのonChangeには、この後実装するTODOのチェック機能を設定します。削除ボタンのonClickにはこの後実装するTODO削除機能を設定します。

TODOのチェック機能の実装

TODOのチェック機能を実装します。TODOのチェック機能はcheckToDo関数として作成します。useToDo.jsに以下を参考にしてTODOのチェック機能を追加しましょう。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp, setDoc, doc } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos =() => {
                      
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
  const [editingId, setEditingId] = useState("");
  const [editedToDo, setEditedToDo] = useState("");
                      
  const getToDos = async () => {
    try{
      const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
      const snapShot = await getDocs(data);
      setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
      setIsUpdated(false);
    }catch{
      alert("Failed to get ToDos.");
    }    
  }
                      
  const addToDo = async () => {
    if (additingToDo&&addedToDo !== "") {
      try{
        await addDoc(collection(db, "users", user.uid, "TODO"), {task:addedToDo, created_date: serverTimestamp(), finished: false});
        setIsUpdated(true);
      }catch{
        alert("Failed to add a ToDo.");
      } 
    }else{
      setAddedToDo( "" );
    }
    setAdditingToDo(!additingToDo);
  }
                      
  const editToDo = async (id) => {
    if (id === editingId ) {
      try{
        await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
        setEditedToDo("");
        setIsUpdated(true);
      }catch{
        alert("Failed to edit the ToDo.");
      } 
      setEditingId("");
    }else{
      setEditingId(id);
    }
  }
                      
  const checkToDo = async (event,id) => {
    await setDoc(doc(db, "users", user.uid, "TODO", id), { finished: event.target.checked }, { merge: true });
    setIsUpdated(true);
  }
                               
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo, checkToDo };
                      
}
                      
export { useToDos };

TODOのチェック機能を実行するのは以下に示すcheckToDo関数になります。

const checkToDo = async (event,id) => {
  await setDoc(doc(db, "users", user.uid, "TODO", id), { finished: event.target.checked }, { merge: true });
  setIsUpdated(true);
}

関数の引数"event"はチェックボックスのイベントオブジェクト、"id"はデータベースのドキュメントIDになります。関数は最初にサブコレクション"TODO"の指定のIDのドキュメントのfinishedフィールドにチェックボックスの状態の上書きを実行します。チェックボックスがチェックされていればtrue、されていなければfalseになります。こちらは非同期処理となります。上書きが完了すると、isUpdatedをtrueに更新します。

useToDo.jsに記述できたら、List.jsxに追加しましょう。以下に習ってList.jsxを書き換えます。

List.jsx
import { useEffect, memo } from "react";
import { toDoStyle, listStyle, editButton } from "../styles";
import { useToDos } from "../hooks/useToDos";
                      
const List = memo(() => {
  const { isUpdated, loadedCollection, getToDos, checkToDo } = useToDos();  //checkToDoを追加
                      
  const onChangeCheckBox = (event, id) => checkToDo(event, id); //追加
                      
  useEffect(() => {
    getToDos();
  }, [isUpdated]);
                      
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map((document) => (
          document.task && (
            <li key={document.id}>
              <div style={toDoStyle}>
                <label style={{ textDecoration: document.finished ? "line-through" : "none" }}>
                  {document.task}
                  <input type="checkbox" onChange={(event) => onChangeCheckBox(event, document.id)} checked={document.finished} />  {/*(event)=>{onChangeCheckBox(event,document.id)を追加*/}
                </label>
                <button onClick={() => {}} style={editButton}>
                  削除
                </button>
              </div>
            </li>
          )
        ))}
      </ul>
    </div>
  );
});
                      
export { List };

コメントが付与された部分が新たに追加した部分になります。useToDoからcheckToDoを追加で取得しています。その後、TODOの完了状態を切り替えるonChangeCheckBox 関数の定義を追加しています。このonChangeCheckBox関数をチェックボックスのonChangeにイベントオブジェクトを引数として設定しています。

TODO削除機能の実装

TODO削除機能を実装します。TODO削除機能はdeleteToDo関数として作成します。useToDo.jsに以下を参考にしてTODO削除機能を追加しましょう。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp, setDoc, doc } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos =() => {
                      
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
  const [editingId, setEditingId] = useState("");
  const [editedToDo, setEditedToDo] = useState("");

  const getToDos = async () => {
      try{
          const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
          const snapShot = await getDocs(data);
          setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
          setIsUpdated(false);
      }
      catch{
          alert("Failed to get ToDos.");
      }    
  }

  const addToDo = async () => {
    if (additingToDo&&addedToDo !== "") {
      try{
        await addDoc(collection(db, "users", user.uid, "TODO"), {task:addedToDo, created_date: serverTimestamp(), finished: false});
        setIsUpdated(true);
      }catch{
        alert("Failed to add a ToDo.");
      } 
    }else{
      setAddedToDo( "" );
    }
    setAdditingToDo(!additingToDo);
  }

  const editToDo = async (id) => {
    if (id === editingId ) {
      try{
        await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
        setEditedToDo("");
        setIsUpdated(true);
      }catch{
        alert("Failed to edit the ToDo.");
      } 
      setEditingId("");
    }else{
      setEditingId(id);
    }
  }

  const checkToDo = async (event,id) => {
    await setDoc(doc(db, "users", user.uid, "TODO", id), { finished: event.target.checked }, { merge: true });
    setIsUpdated(true);
  }

  const deleteToDo = async (id) => {
    await deleteDoc(doc(db, "users", user.uid, "TODO", id));
    setIsUpdated(true);
  }
       
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo, checkToDo, deleteToDo };
                      
}
                      
export { useToDos };

TODO削除機能を実行するのは以下に示すdeleteToDo関数になります。

const deleteToDo = async (id) => {
  await deleteDoc(doc(db, "users", user.uid, "TODO", id));
  setIsUpdated(true);
}

関数の引数"id"はデータベースのドキュメントIDになります。関数は最初にサブコレクション"TODO"内のidに一致するドキュメントを削除します。こちらは非同期処理となります。削除が完了すると、isUpdatedをtrueに更新します。

useToDo.jsに記述できたら、List.jsxに追加しましょう。以下に習ってList.jsxを書き換えます。

List.jsx
import { useEffect, memo } from "react";
import { toDoStyle, listStyle, editButton } from "../styles";
import { useToDos } from "../hooks/useToDos";
                      
const List = memo(() => {
  const { isUpdated, loadedCollection, getToDos, checkToDo, deleteToDo } = useToDos();  //deleteToDoを追加
                      
  const onChangeCheckBox = (event, id) => checkToDo(event, id);
  const onClickDelete = (id) => deleteToDo(id); //追加
                      
  useEffect(() => {
    getToDos();
  }, [isUpdated]);
                      
  return (
    <div style={listStyle}>
      <ul>
        {loadedCollection.map(
          (document) =>
            document.task && (
              <li key={document.id}>
                <div style={toDoStyle}>
                  <label
                    style={{
                      textDecoration: document.finished ? "line-through" : "none",
                    }}
                  >
                    {document.task}
                    <input
                      type="checkbox"
                      onChange={(event) => onChangeCheckBox(event, document.id)}
                      checked={document.finished}
                    />
                  </label>
                  <button onClick={() => onClickDelete(document.id)} style={editButton}>
                    削除
                  </button> {/*()=>onClickDelete(document.id)を追加*/}
                </div>
              </li>
            )
        )}
      </ul>
    </div>
  );
});
                      
export { List };

コメントが付与された部分が新たに追加した部分になります。useToDoからdeleteToDoを追加で取得しています。その後、TODOを削除するonClickDelete関数の定義を追加しています。onClickDelete関数を削除ボタンのonClickにドキュメントIDを引数として設定しています。以上で、List.jsxの作成が完了するとともに、全てのコンポーネントの作成が完了しました。

エラー処理の実装

最後にuseToDO.jsにて定義したcheckToDoとdeleteToDoに対してもエラー処理を追加して、コード作成は完了となります。エラー処理を実装したuseToDO.jsが以下になります。

useToDo.js
import { useState } from "react";
import { collection, getDocs, query, orderBy, addDoc, serverTimestamp, setDoc, doc } from "firebase/firestore"; 
import { db } from "../firebase";
import { useContext } from "react";
import { UserInfoContext } from "../context/UserInfoProvider";
                      
const useToDos =() => {
                      
  const { user } = useContext(UserInfoContext);
  const [loadedCollection, setLoadedCollection] = useState([]);
  const [isUpdated, setIsUpdated] = useState(false);
  const [additingToDo, setAdditingToDo] = useState(false);
  const [addedToDo, setAddedToDo] = useState("");
  const [editingId, setEditingId] = useState("");
  const [editedToDo, setEditedToDo] = useState("");

  const getToDos = async () => {
      try{
          const data = query(collection(db, "users", user.uid, "TODO"), orderBy('created_date'));
          const snapShot = await getDocs(data);
          setLoadedCollection(snapShot.docs.map((doc) => ({ ...doc.data(), id:doc.id })));
          setIsUpdated(false);
      }
      catch{
          alert("Failed to get ToDos.");
      }    
  }

  const addToDo = async () => {
    if (additingToDo&&addedToDo !== "") {
      try{
        await addDoc(collection(db, "users", user.uid, "TODO"), {task:addedToDo, created_date: serverTimestamp(), finished: false});
        setIsUpdated(true);
      }catch{
        alert("Failed to add a ToDo.");
      } 
    }else{
      setAddedToDo( "" );
    }
    setAdditingToDo(!additingToDo);
  }

  const editToDo = async (id) => {
    if (id === editingId ) {
      try{
        await setDoc( doc(db, "users", user.uid, "TODO", id), {task:editedToDo},{ merge: true }); 
        setEditedToDo("");
        setIsUpdated(true);
      }catch{
        alert("Failed to edit the ToDo.");
      } 
      setEditingId("");
    }else{
      setEditingId(id);
    }
  }

  const checkToDo = async (event,id) => {
    try{
      await setDoc(doc(db, "users", user.uid, "TODO", id), { finished: event.target.checked }, { merge: true });
      setIsUpdated(true);
    } catch {
      alert("Failed to check the ToDo.");
    } 
}

const deleteToDo = async (id) => {
    try{
      await deleteDoc(doc(db, "users", user.uid, "TODO", id));
      setIsUpdated(true);
    } catch {
      alert("Failed to delete the ToDo.");
    }
}
       
  return { loadedCollection, isUpdated, setIsUpdated, getToDos, additingToDo, setAddedToDo, addToDo, editingId, setEditedToDo, editToDo, checkToDo, deleteToDo };
                      
}
                      
export { useToDos };

エラー処理として、TODOのチェック処理に失敗すると"Failed to check the ToDo."、TODO削除に失敗すると"Failed to delete the ToDo."のメッセージを表示するアラートを設定しています。

コードの作成が完了したらプログラムを実行して、このレッスンの最初で示した画面と同じものが表示されるかや組み込んだ機能が正常に動作するかを各自で確認してください。