見出し画像

[React]Todoリストの作成 – 初学者向け2

[React]Todoリストの作成 – 初学者向け、その2です。
前回は基本的に元の参考記事に準じた内容でしたが、ここからは、元記事以外の、私のオリジナルでカスタマイズした内容となります。より機能のブラッシュアップを行うのと、Firebase-GitHub連携を行い、GitHubにコミットの都度、Firebaseに自動でデプロイする仕組みを構築します。
前回の記事はこちら

前回参考にした記事はこちらです


はじめに

本記事は、Reactの初学者向けのコンテンツです。前回までの内容を更にブラッシュアップを実施し、GitHub、Firebaseとの連携を行い、アプリケーションの更新、自動デプロイを行う環境を構築します。React環境は、Vite+TypeScript
CSS+UIツールとして、TawilwindCSS、Shadcn UI、
ホスティングサービスは、GoogleのFirebaseを利用しています。

6.入力データの保存と読込

前回からの続きと言うことで、6.からスタートです。
入力したTODOデータをローカルストレージに保存し、再読み込みした際にも入力データが消えないようにしたいと思います。
ローカルストレージはブラウザ上に情報を保存する仕組みのことをいいます。簡単にいうとデータを入れておく箱のようなものです。5MBまでのデータ格納が可能です。

ローカルストレージ保管には、Storage: setItem() メソッドを利用します。

下記の通り、追加ボタンを押した際に、同時にローカルストレージに保存します。

          <Button
            className="w-full mt-2"
            onClick={() => {
              setTodos([...todos, todo]);
              setTodo("");
              localStorage.setItem("todos", JSON.stringify([...todos, todo]));
              //ローカルストレージへの保存追加
            }}
          >
            追加
          </Button>

JSON.stringify([...todos, todo])としているのは、Storage: setItem() メソッドが文字列しか保存出来ないため、配列を保存するには、JSON形式に変換してから格納すると言う形になるためです。

ブラウザの検証ツール、アプリケーション、ローカルストレージの箇所に、todosと言うキー名称でデータが保存されているのが分かります。

次にデータ削除時です。こちらも削除処理の際に、同時にローカルストレージに削除後のデータ(削除対象を除外した残りのデータ)を格納します。コードは以下です。

          <ul>
            {todos.map((todo, index) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button
                  className="ml-2"
                  onClick={() => {
                    setTodos(todos.filter((_, i) => i !== index));
                    localStorage.setItem("todos", JSON.stringify(todos.filter((_, i) => i !== index)));//ローカルストレージへの保存
                  }}
                >
                  <MdDelete color="red" />
                </button>
              </li>
            ))}
          </ul>

setTodoの処理と同様に、filterされたデータをtodosとして格納しています。

続いて、ページを再読み込みや、画面を閉じ後に再度アクセスした場合の処理です。
ローカルストレージにデータが保存されていない場合は、再読み込み、再アクセスにより入力されたデータは初期化/無くなってしまいますが、ローカルストレージの保存している場合はデータの読み込みにより、データの復活が実現出来ます。
この処理は、Reactの機能(hook)である、useEffectを利用します。

コードは以下のとおりです。

  useEffect(() => {
    const storedTodo = localStorage.getItem("todos");
    if (storedTodo) {
      setTodos(JSON.parse(storedTodo));
    }
  }, []);

最後にある,[])の配列の箇所は、useEffectの処理を行う、トリガーを記載します。この例だと、[]と空の配列としてますので、ページ読み込み時のみ実行すると言う形です。
内容は、ローカルストレージに保存されたデータをJSONに形式として抽出し、setTodosに渡しています。これをページ読み込み時に実行しています。

全体のコードは下記のとおりです。

// /src/App.tsx
import { useEffect, useState } from "react";
import { Button } from "./components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Input } from "./components/ui/input";
import { MdDelete } from "react-icons/md"; 

function App() {
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState<string[]>([]);

    //ローカルストレージに保存されたtodoの自動読込、追加
  useEffect(() => {
    const storedTodo = localStorage.getItem("todos");
    if (storedTodo) {
      setTodos(JSON.parse(storedTodo));
    }
  }, []);

  return (
    <div className="bg-gray-100 flex justify-center items-center min-h-screen">
      <Card className="w-[400px]">
        <CardHeader>
          <CardTitle>TODO App</CardTitle>
        </CardHeader>
        <CardContent>
          <Input
            placeholder="タスクを追加"
            onChange={(e) => setTodo(e.target.value)}
            value={todo}
          />
          <Button
            className="w-full mt-2"
            onClick={() => {
              setTodos([...todos, todo]);
              setTodo("");
              localStorage.setItem("todos", JSON.stringify([...todos, todo]));//ローカルストレージへの保存追加
            }}
          >
            追加
          </Button>
          <ul>
            {todos.map((todo, index) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button
                  className="ml-2"
                  onClick={() => {
                    setTodos(todos.filter((_, i) => i !== index));
                    localStorage.setItem("todos", JSON.stringify(todos.filter((_, i) => i !== index)));//ローカルストレージへの保存追加
                  }}
                >
                  <MdDelete color="red" /> 
                </button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}

export default App

7.入力データチェック

次に入力データの有無チェックです。
今の状態だと、入力データが空であっても登録処理が出来てしまいます。

データが入力されているかチェックを行い、データが空の場合はエラーを表示する仕組みを実装します。下記の通り、新たにerror用のステートを設けます。
追加ボタンクリック時、入力データ(todo)の有無をチェックし、無しの場合は、setErrorでエラーメッセージをセットし、エラーメッセージを表示します。

  const [error, setError] = useState<string | null>(null); // エラーメッセージ用のstateを追加
<Input
   placeholder="タスクを追加"
   onChange={(e) => setTodo(e.target.value)}
   onFocus={() => setError(null)}//フィールドにフォーカスした際にエラーメッセージ消去
   value={todo}
/>
 {error && <div className="text-red-500">{error}</div>} {/*errorなら エラーメッセージを表示 */}
   <Button
     className="w-full mt-2"
     onClick={() => {
        if (todo === "") {
          setError("タスクを入力してください");//エラーメッセージをセットする
          return;
        }
        setTodos([...todos, todo]);
        setTodo("");
        localStorage.setItem("todos", JSON.stringify([...todos, todo]));//ローカルストレージへの保存追加
      }}
  >追加</Button>

なお、再度入力フィールドにフォーカスした際にエラー表示が消えるように、onFocus={() => setError(null)}でerrorを初期化しています。
結果、以下のようになります。エラーチェックとエラー表示が出来るようになりました。

コード全体は以下のとおりです。

// ~/src/App.tsx
import { useEffect, useState } from "react";
import { Button } from "./components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Input } from "./components/ui/input";
import { MdDelete } from "react-icons/md";

function App() {
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState<string[]>([]);
  const [error, setError] = useState<string | null>(null); // エラーメッセージ用のstateを追加

  //ローカルストレージに保存されたtodoの自動読込、追加
  useEffect(() => {
    const storedTodo = localStorage.getItem("todos");
    if (storedTodo) {
      setTodos(JSON.parse(storedTodo));
    }
  }, []);

  return (
    <div className="bg-gray-100 flex justify-center items-center min-h-screen">
      <Card className="w-[400px]">
        <CardHeader>
          <CardTitle>TODO App</CardTitle>
        </CardHeader>
        <CardContent>
          <Input
            placeholder="タスクを追加"
            onChange={(e) => setTodo(e.target.value)}
            onFocus={() => setError(null)}//フィールドにフォーカスした際にエラーメッセージ消去
            value={todo}
          />
          {error && <div className="text-red-500">{error}</div>} {/*errorなら エラーメッセージを表示 */}
          <Button
            className="w-full mt-2"
            onClick={() => {
              if (todo === "") {
                setError("タスクを入力してください");//エラーメッセージをセットする
                return;
              }
              setTodos([...todos, todo]);
              setTodo("");
              localStorage.setItem("todos", JSON.stringify([...todos, todo]));//ローカルストレージへの保存追加
            }}
          >
            追加
          </Button>
          <ul>
            {todos.map((todo, index) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button
                  className="ml-2"
                  onClick={() => {
                    setTodos(todos.filter((_, i) => i !== index));
                    localStorage.setItem("todos", JSON.stringify(todos.filter((_, i) => i !== index)));//ローカルストレージへの保存追加
                  }}
                >
                  <MdDelete color="red" />
                </button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}

export default App

8.重複データチェック

データの有無チェックは出来ました。次は、登録しようとしているデータの重複チェック機能の実装です。通常のTODOでタイトルが合致するケースはあまりないかも知れないですが、今回のプロトタイプでは、掃除、洗濯、料理など、簡単に重複しそうな内容が多いので、重複チェックを設けてみました。

重複のチェックには、includes()を使用しています。

<Input
   placeholder="タスクを追加"
   onChange={(e) => setTodo(e.target.value)}
   onFocus={() => setError(null)}//フィールドにフォーカスした際にエラーメッセージ消去
  value={todo}
/>
 {error && <div className="text-red-500">{error}</div>} {/*errorなら エラーメッセージを表示 */}
 <Button
   className="w-full mt-2"
   onClick={() => {
     if (todo === "") {
       setError("タスクを入力してください");//エラーメッセージをセットする
     } else if (todos.includes(todo)) {// includesによる重複タスクのチェック
       setError("このタスクは既に存在します"); //エラーメッセージをセットする
     } else {
       setTodos([...todos, todo]);
       setTodo("");
       localStorage.setItem("todos", JSON.stringify([...todos, todo]));//ローカルストレージへの保存追加
     }}}
 >追加</Button>

結果、以下のような動きとなります。

なお、エラー表示に伴い、フィールドの初期化も考えたのですが、どうも使い勝手が良くない感があり(何を入力したからエラーになったかが分からないようになりそうで)そのまま残す形にしました。

コード全体は以下です。

// ~/src/App.tsx
import { useEffect, useState } from "react";
import { Button } from "./components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Input } from "./components/ui/input";
import { MdDelete } from "react-icons/md";

function App() {
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState<string[]>([]);
  const [error, setError] = useState<string | null>(null); // エラーメッセージ用のstateを追加

  //ローカルストレージに保存されたtodoの自動読込、追加
  useEffect(() => {
    const storedTodo = localStorage.getItem("todos");
    if (storedTodo) {
      setTodos(JSON.parse(storedTodo));
    }
  }, []);

  return (
    <div className="bg-gray-100 flex justify-center items-center min-h-screen">
      <Card className="w-[400px]">
        <CardHeader>
          <CardTitle>TODO App</CardTitle>
        </CardHeader>
        <CardContent>
          <Input
            placeholder="タスクを追加"
            onChange={(e) => setTodo(e.target.value)}
            onFocus={() => setError(null)}//フィールドにフォーカスした際にエラーメッセージ消去
            value={todo}
          />
          {error && <div className="text-red-500">{error}</div>} {/*errorなら エラーメッセージを表示 */}
          <Button
            className="w-full mt-2"
            onClick={() => {
              if (todo === "") {
                setError("タスクを入力してください");//エラーメッセージをセットする
              } else if (todos.includes(todo)) {// includesによる重複タスクのチェック
                setError("このタスクは既に存在します"); //エラーメッセージをセットする
              } else {
                setTodos([...todos, todo]);
                setTodo("");
                localStorage.setItem("todos", JSON.stringify([...todos, todo]));//ローカルストレージへの保存追加
              }
            }}
          >追加</Button>
          <ul>
            {todos.map((todo, index) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button
                  className="ml-2"
                  onClick={() => {
                    setTodos(todos.filter((_, i) => i !== index));
                    localStorage.setItem("todos", JSON.stringify(todos.filter((_, i) => i !== index)));//ローカルストレージへの保存追加
                  }}
                >
                  <MdDelete color="red" />
                </button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}

export default App

9.GitHub、Firebaseとの連携

コードの開発は完了しましたので、続いて、リポジトリの作成とGitHubとの連携、更に、GitHubとFirebaseを連携させ、GitHubにプッシュ・発行されれば自動的にFirebaseにデプロイされる仕組みを構築したいと思います。

まず、GitHubとの連携です。
前提として、GitHubアカウント及び利用する環境は既にあるものとします。
GitHub環境の準備等については、こちらを参考にしてください。

GitHubでのブランチの作成、コミット、プッシュはVSCode上で簡単に実施出来ます。
まずリポジトリの作成を行います。下記画像の通り、左側のメニューペインで赤丸の箇所(ソース管理)をクリックして、リポジトリを初期化をクリックします。

これでローカル上のリポジトリが作成出来ましたので、
これまで作成してきたプロジェクトデータをコミットします。以下のGIF動画を参照してください。
コミット時は、コメント(コミットの意図、位置づけを定義する)が必要ですので適当/適切なコメントを入力の上、右上にある✓ボタンをクリックします。色々ポップアップが出ますが、「はい」で大丈夫です。

ローカルリポジトリのコミットが完了したら、リモートリポジトリ(GitHub)にブランチ発行・プッシュします。ブランチの発行をクリックすると、GitHubのリポジトリ作成の名前候補が表示されますので、名前を設定し、発行を行います。なお、Privateは一般非公開のもの、Publicは公開のものです。どちらかを選択します。成功すれば下記のような成功メッセージが表示されます。

これでGitHubとの連携、リポジトリのブランチ発行が出来ました。

次にFirebaseとGitHubを連携させ、GitHubにコミット、プッシュされると、Firebaseに自動でデプロイされる仕組みの実装を行います。

ターミナルで、以下のコマンドを入力します(前回の記事でFirebaseの設定を実施していますが、その設定を実施済の前提です)

$ firebase init hosting:github

そうすると、ブラウザ上でGitHubアカウントの認証を要求されますので認証を行います。

GitHubのアカウント認証に成功するとコマンドラインにも以下のようなメッセージが出ます。
✔ Success! Logged into GitHub as xxxxx

続けてターミナル上で対話式に設定を進めていきます。

? For which GitHub repository would you like to set up a GitHub workflow? (format: user/repository) 
your-github-account/your-repository-name
#アカウント名/リポジトリ名をに入力します

✔  Created service account github-action-844093317 with Firebase Hosting admin permissions.
✔  Uploaded service account JSON to GitHub as secret FIREBASE_SERVICE_ACCOUNT_MY_FIRST_TODO_619B6.
i  You can manage your secrets at https://github.com/your-github-account/your-repository-name/settings/secrets.

? Set up the workflow to run a build script before every deploy? Yes
#yesで
? What script should be run before every deploy? npm ci && npm run build
#ビルド時のコマンドの指定です。npmの方は上記で、yarnの場合は、yarn –frozen-lockfile && yarn run build

✔  Created workflow file /Volumes/DEV/work/first-todo/.github/workflows/firebase-hosting-pull-request.yml
#github、アクションワークフロー用のymlの生成しましたのメッセージ
? Set up automatic deployment to your site's live channel when a PR is merged? Yes
#Yesで
? What is the name of the GitHub branch associated with your site's live channel? main
#ブランチ名を指定します。ブランチ名を変更してなければ、(デフォルトの)mainで大丈夫です。違う設定をしている方は
#そのブランチ名を入力
✔  Created workflow file /Volumes/DEV/work/first-todo/.github/workflows/firebase-hosting-merge.yml
#github、アクションワークフロー用のymlの生成しましたのメッセージ
i  Action required: Visit this URL to revoke authorization for the Firebase CLI GitHub OAuth App:
https://github.com/settings/connections/applications/89cf50f02ac6aaed3484
i  Action required: Push any new workflow file(s) to your repo

i  Writing configuration info to firebase.json...
i  Writing project information to .firebaserc...

✔  Firebase initialization complete!
#このメッセージが出れば、GitHub連携の設定終了です。

これでGitHubとFirebaseの連携設定は完了です。
この処理によって、プロジェクトルートフォルダ配下の/.github/workflowsと言うフォルダにGitHubにコミット時にGitHub側で自動実行されるactionの設定ファイル(yaml形式)が生成されます。
GitHub ActionsはGitHubがサービスの一環として提供する、ワークフロー自動化サービスです。この機能を使うことにより、自動でビルドし、Firebaseに連携させます。

ここまで実施した変更をコミットします。
VSCode上から先に容量でローカルgitにコミットの上、GItHubにプッシュします。
下図で①がコミット、②がGitHubにプッシュです。

これに伴い、Firebaseのセットアップで生成された、GitHubのAction用yamlファイルもプッシュされますので、これにより、GitHubにて自動でビルド処理とFirebaseへの連携がなされます。

GitHubの該当レポジトリのActionsタブで下記が表示されていれば、Actionsのworkflow実行成功です。

また、FirebaseのHostingの箇所を確認してみましょう。
下記のようなメッセージが出てればGitHubとの連携でのデプロイ成功です。

ドメインの箇所からリリースされたアプリケーションを確認してみましょう。

アプリケーション画面が出てくれば成功です!
という事で、コードの機能ブラッシュアップ、GitHubとの連携、更にFirebaseとの連携で、コミット→自動デプロイのフローを構築することが出来ました。
どなかたかの参考になれば幸いです。またご意見やアドバイスをいただけると嬉しいです。今後も学び続けたいと思います。

※本記事は、下記の自身のサイトの記事の転載となります。


いいなと思ったら応援しよう!