見出し画像

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

少し前からReactの勉強をしてます。
本日ご紹介するのは、よくある、Todoリストの作成です。
こちらの記事を参考にしています。


はじめに

本記事は、Reactの初学者向けのコンテンツです。基本は上記サイトの内容そのまま試しています。所々、追加の説明を記載したり、追加でカスタマイズをしてたりもしてます。Reactの基本的な使い方から実際にアプリケーションをデプロイしてユーザーが使えるリリースまでを行えます。
React環境は、Vite+TypeScript
CSS+UIツールとして、TawilwindCSS、Shadcn UI、
ホスティングサービスは、GoogleのFirebaseを利用しています。

1.環境構築

まずは、環境構築です。
JavaScript実行環境のnode.jsとパッケージマネージャのnpmをインストールします。
元記事では、macでのインストールはbrewを使う形になってますが、私の環境では、brewがうまくパスが通らないので、無難にインストーラパッケージをインストールしました。Windowsの方も同じくインストーラからインストールです。

$ node -v
20.16.0
$ npm -
10.8.1

次にViteを利用してReactプロジェクトを作成します。
npm create vite@latest でプロジェクト作成を実行します。
プロジェクト名は「first-todo」で
他の選択肢は下記の通りです。

$ npm create vite@latest✔ Project name: … first-todo
✔ Select a framework: › React
✔ Select a variant: › TypeScript

これでReactの初期環境構築は完了です。
続けて出てくる内容に従って、環境起動を行います。

$ cd first-todo
$ npm i
$ npm run dev

npm i は、プロジェクト作成時に生成された、プロジェクトのライブラリの依存環境が記載されたpackage.jsonに明示されているすべてのパッケージをインストールします。
npm run devは、開発用のサーバの起動を行います。これにより、ブラウザで開発プロジェクトの実行結果を確認出来ます。通常開発サーバは、http://localhost:5173/
で起動されます。

localhost:5173にアクセスすると以下の画面が表示されます

次に、Shadcnを導入します
サーバを起動したターミナルで「ctrl-c」を入力し一度サーバ停止するか、別のターミナルを開いてfirst-todoディレクトリに移動してインストールします

$ npm i -D tailwindcss postcss autoprefixer
$ npx tailwindcss init -p
$ npm i -D @types/node

npm i -D はnpm i --save-devの略です。
--save-devはローカルインストールの事で、プロジェクト内のみ有効なインストールを行う場合に実行します。@types/nodeは、TypeScriptの為、型定義の情報が必要なので、インストールします。

VSCode等のエディタでfirst-todoディレクトリを開きます。なお、ローカルで開発される方は、拡張性の高さ、使い勝手の良さ、作業効率などで、VSCodeが圧倒的にお薦めです。

そして、ディレクトリ直下のtsconfig.jsonを変更します

/* /tsconfig.json */
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

"references": [{ "path": "./tsconfig.node.json" }]の箇所でエラーが発生する場合は、コメントアウトしてしまってください。私はビルド時エラーが発生し、試行錯誤した後、コメントアウトして回避しました。特に影響は出ていないです。
なお、tsconfig.node.jsonは、Node.js環境で実行されるサーバーサイドコードに関する設定を管理するもので、Node.js向けに最適化された設定を使うことで、サーバーサイドアプリケーションやスクリプトの開発をサポートする役割です。今回については、サーバーサイドは関係ありませんので、影響は無いと思います。

同様に直下のvite.config.tsを変更します

// /vite.config.ts
import path from "path"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
 
export default defineConfig({
  plugins: [react()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
})

ターミナルに戻りshadcn/uiをインストールします

$ npx shadcn-ui@latest init

✔ Would you like to use TypeScript (recommended)? … no / yes
✔ Which style would you like to use? › Default
✔ Which color would you like to use as base color? › Slate
✔ Where is your global CSS file? … src/index.css
✔ Would you like to use CSS variables for colors? … no / yes
✔ Are you using a custom tailwind prefix eg. tw-? (Leave blank if not) … 
✔ Where is your tailwind.config.js located? … tailwind.config.js
✔ Configure the import alias for components: … @/components
✔ Configure the import alias for utils: … @/lib/utils
✔ Are you using React Server Components? … no / yes
✔ Write configuration to components.json. Proceed? … yes

ポイントはReacct Server ComponentをNoにすることです。
サーバーコンポーネントは特に使用しない為です。
これで導入が完了したので、ボタンを表示してみます

まずはボタンをインストールします

$ npx shadcn-ui@latest add button

一度開発サーバーを落とした場合は、npm run dev コマンドで、再度、開発サーバを起動します。以降インストールする際に開発サーバーを停止してインストールコマンドを実行した場合は、同様に操作します。(開発サーバー起動したままで別のターミナルウィドウからインストールコマンドを実行することも可能です)

次にVSCode等のエディタでコードを記載していきます。
プロジェクトルートのsrcフォルダ内、App.tsxを開き、以下のようにコードを記載します。
以下は、App.tsxの初期内容です。これらは不要ですので、一旦全て削除します。

// /src/App.tsx
//↓全て削除します
import { useState } from 'react'
import reactLogo from './assets/react.svg'
import viteLogo from '/vite.svg'
import './App.css'

function App() {
  const [count, setCount] = useState(0)

  return (
    <>
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src={viteLogo} className="logo" alt="Vite logo" />
        </a>
        <a href="https://react.dev" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button onClick={() => setCount((count) => count + 1)}>
          count is {count}
        </button>
        <p>
          Edit <code>src/App.tsx</code> and save to test HMR
        </p>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </>
  )
}

export default App

次に以下のように記載します。必要な「枠」だけ残す形です。

// /src/App.tsx
function App() {
  
  return (
    <>
      
    </>
  )
}

export default App

次にshadcnのボタンを配置してみます。

// /src/App.tsx
import { Button } from "./components/ui/button"

function App() {
  
  return (
    <>
    <div>
      <Button>Click me</Button>
    </div>
    </>
  )
}

export default App

ちなみに、Reactコンポーネントを挿入する場合は(上記で言えば<Button>)、<Button>と入力すれば、VSCodeが自動で関連すると思われるコンポートのインポートを自動で補完してくれます。
下記は参考のGIF動画です。importされていなくても、コンポーネントタグの最後の一文字を削除しまた記載すればimportの候補先が表示され、自動でコードに挿入してくれます。

また、/src/main.tsxについては、CSSのインポートを変更します(ここは元記事には掲載なし)。

// /src/main.tsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.tsx'
import '../app/globals.css' //import './index.css'を変更

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

localhost:5173をみると黒いボタンが表示されます

shadcnの導入ができたようです。

2.TODOアプリのデザインを作る

まずはTODOアプリのデザイン部分を作成です。
機能面はこのあと作成していきます

shadcnのCardとInputを利用しますhttps://ui.shadcn.com/docs/components/card
https://ui.shadcn.com/docs/components/input

ターミナルで以下コマンドを実行し、CardとInputを追加します。

$ npx shadcn-ui@latest add card
$ npx shadcn-ui@latest add input

次に/src/App.tsxに以下のようにコードを記載します。

// /src/App.tsx
import { Button } from "./components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Input } from "./components/ui/input";

function App() {
  const todos = ["掃除する", "洗濯する", "料理する"];
  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="タスクを追加" />
          <Button className="w-full mt-2">追加</Button>
          <ul>
            {todos.map((todo) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button className="ml-2">削除する</button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}
export default App

ReactではHTMLの部分とjsの部分を1つのファイルに書くことができます(JSXと呼ばれます)

          <ul>
            {todos.map((todo) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button className="ml-2">削除する</button>
              </li>
            ))}
          </ul>

このようにHTMLのなかでJavaScriptのmapを回してを作ることでTODOを表示しています
スタイリングにはTailwindCSSを使っています

<div className="bg-gray-100 flex justify-center items-center min-h-screen">

TailwindCSSは、上記のようにcssをクラスで当てることができます
bg-gray-100 : 背景色
flex : display : フレックスにする
justiyf-center : 要素は右左で中心に配置
items-center : 要素を上下で中心に配置
このようにCSSを細かく当てなくてもできるのがTailwindCSSなどのUIユーティリティのメリットです

<Card>など大文字で始まるタグはShadcnのコンポーネントで、タグを利用することで簡単におしゃれなデザインが利用可能です
以下のような画面イメージになってると思います。

3.TODOを追加できるようにする

// /src/App.tsx
import { useState } from "react"; //追加
import { Button } from "./components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card";
import { Input } from "./components/ui/input";

function App() {
  const [todo, setTodo] = useState(""); //追加
  const todos = ["掃除する", "洗濯する", "料理する"];
  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} //追加
          />
          <div>{todo}</div> {/*追加(HTMLコード中のコメントはこう記載します)*/}
          <Button className="w-full mt-2">追加</Button>
          <ul>
            {todos.map((todo) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button className="ml-2">削除する</button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}

export default App

ここまで実装すると入力した内容がフォームの下に表示されるようになります

画面の状態を変更したい場合(ここではtodoの内容)はuseStateを使います

const [todo, setTodo] = useState("");

todoは現在の値
setTodoはtodoを変更するための関数
初期値は””(空)になっています

useStateは、const [ state, setState] と、値とそれを変更操作するためのset関数の組み合わせで、慣例的にset関数はset+最初大文字の値名とする事が多い(上記でいうと、state, setState)です。

フォームに注目します

      <Input
        placeholder="タスクを追加"
        onChange={(e) => setTodo(e.target.value)} //追加
      />

フォームではonChangeというのを追加しました
これはフォームの内容が変わったら実行する関数を書くことができます
setTodoを使うことで入力した内容を反映しています

なので入力をするとtodoの内容が変わります

      <div>{todo}</div>

では、TODOを追加できるように変更していきましょう

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

function App() {
  const [todo, setTodo] = useState("");
  const [todos, setTodos] = useState<string[]>([]); // 追加
  // const todos = ["掃除する", "洗濯する", "料理する"]; //コメントにする
  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)}
          />
          {/* <div>{todo}</div> */}
          <Button
            className="w-full mt-2"
            onClick={() => {
              setTodos([...todos, todo]);
            }} // 追加
          >
            追加
          </Button>
          <ul>
            {todos.map((todo) => (
              <li className="bg-white p-2 mt-2 flex">
                <div>・{todo}</div>
                <button className="ml-2">削除する</button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}
export default App

テストデータをコメントにしました

// const todos = ["掃除する", "洗濯する", "料理する"];

追加ボタンをおすとtodosの内容が追加されて、画面の変更が行われるのでこういうものはuseStateを使って更新する必要があるので新たに用意します

const [todos, setTodos] = useState<string[]>([]);

ここでstring[]としているのはtodosに入るデータの型を定義しています
[]は配列であることを示しています
todosにはstringの配列しか入らないことを書いています
初期値は[]なので、空配列になります

ボタンにonClickを追加します

      <Button
        className="w-full mt-2"
        onClick={() => {
          setTodos([...todos, todo]);
        }}
      >

onClickはボタンを押したときに実行する関数を書けます

setTodos([...todos, todo]);

このようにいま表示されているtodosの後ろにtodoを追加して更新していますtodosが[“A” “B”]でCを追加したとしたらsetTodos([“A”, “B”, “C”])と同じになります

ここで以下の疑問が出た人がいるかと思います

setTodos(todos.push(todo));

いまあるtodosという配列にtodoを追加する実装です。
しかしこれでは画面に変化が起きません

useStateはセットされているものが変更されたかをみています
セットされたリストの中身が変更されているかはみていないのです

あくまでリストが変更されたことだけを注目しています

ここで追加ボタンを押してフォームをいちいち消すのが大変と気づくでしょう
追加ボタンを押したらtodoを空にする実装もしましょう

      <Button
        className="w-full mt-2"
        onClick={() => {
          setTodos([...todos, todo]);
          setTodo("") // 追加、setTodoでtodoを空に
        }}
      >

なお、元記事には記載が無いのですが、実は上記のtodoを空にするは、そのままだとinput内のデータは空になりません。inputの箇所に以下の記載が必要です。value={todo}です。これがないと、todoを空にしても、inputのフィールド上に反映されません。

      <Input
        placeholder="タスクを追加"
        onChange={(e) => {
          setTodo(e.target.value);
        }}
        value={todo}/*フィールドが初期化されないので追加*/
        />

このinputタグのvalueは結構ハマり要素だったりします。通常valueはonChangeとの組み合わせを条件に編集可能な状態になりますが、stateの使い方によっては、うまく機能せず、編集不可(reaaOnly)の扱いに処理されることがあります。この辺は、また別の機会に

4.削除機能

次に削除機能を作ります
削除するボタンを押したら削除するように実装します

        {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));
              }}
            >
              削除する
            </button>
          </li>
        ))}

まずはmapでインデックス番号(何番目のtodoか)をわかるようにします

todos.map((todo, index) => (

filter関数を使って、todosの番号と削除したいtodoのインデックスを比較して一致たものを配列から取り除きます(一致しないものだけを配列に残します)

              onClick={() => {
                setTodos(todos.filter((_, i) => i !== index));
              }}

あとは削除するボタンが微妙なのでいい感じのアイコンにします
ここではreact-iconsを利用します

$ npm i react-icons

今回は以下のアイコンを利用します
https://react-icons.github.io/react-icons/search/#q=delete

// /src/App.tsx
import { 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 todos = ["掃除する", "洗濯する", "料理する"];
  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}/*フィールドが初期化されないので追加*/
          />
          {/* <div>{todo}</div> */}
          <Button
            className="w-full mt-2"
            onClick={() => {
              setTodos([...todos, todo]);
              setTodo("");
            }}
          >
            追加
          </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));
                  }}
                >
                  <MdDelete color="red" /> {/* 変更 */}
                </button>
              </li>
            ))}
          </ul>
        </CardContent>
      </Card>
    </div>
  );
}

export default App

いい感じです

5.Firebaseへのデプロイ

アプリケーションができたのであとはユーザーに利用してもらうためにFirebaseを使ってデプロイを行います

アカウントが必要なので以下から作成して下さい

アカウントができると以下の画面にいくので、+ボタン(プロジェクトを追加)をクリックします

プロジェクト名は適当につけて大丈夫です

Googleアナリティクスを無効にして「プロジェクトを作成」をクリック

歯車マークからプロジェクトの設定をクリック

下まで移動して>マークをクリック

アプリのニックネームをmy-first-todoにして「このアプリをFirebase Hostingも設定します」をチェックして「アプリを登録」をクリック

ここまでできたらターミナルに戻ってfirebaseのツールをインストールします

$ npm install firebase
$ npm install -g firebase-tools
$ firebase login
#メール確認をしてください

$ firebase init hosting
? Please select an option: Use an existing project
? Select a default Firebase project for this directory: my-first-todo-6b04c (my-first-todo)
i  Using project my-first-todo-6b04c (my-first-todo)
#先ほど作成したプロジェクトを選んでください

? Detected an existing Vite codebase in the current directory, should we use this? (Y/n) 
#ここで必ずエンターを押してください

=== Hosting Setup

Your public directory is the folder (relative to your project directory) that
will contain Hosting assets to be uploaded with firebase deploy. If you
have a build process for your assets, use your build's output directory.

? What do you want to use as your public directory? dist
# distを選択してください
? Configure as a single-page app (rewrite all urls to /index.html)? Yes

? Set up automatic builds and deploys with GitHub? (y/N) N
# ここでは一旦Noとしてください。元記事には無いですが、gitHub連携は、後日触れます。

firebaseのpublic directpryの指定でdistを指定してますので、プロジェクトのビルドを行います。distはビルドしたデータが格納される場所です。ターミナルで下記コマンドを投入します。

$ npm run build

これでデプロイの設定が完了したのでfirebaseへのデプロイを行います

$ firebase deploy

✔  Deploy complete!

Project Console: https://console.firebase.google.com/project/my-first-todo-6b04c/overview
Hosting URL: https://my-first-todo-6b04c.web.app

少し待つとURLが発行されるのでアクセスします

デプロイができました
このアプリはスマホからでもアクセスができるのでユーザー利用が可能になります

これ以降は、私自身でカスタマイズしたものを記載しようと思ってましたが、記事がだいぶ長くなってしまったので、今回はここまでとします。
続編では、データ入力のエラーチェックや、ローカルストレージへのデータ保存、GitHubとFirebaseとの連携等について触れたいと思います。

※こちらの記事は、自身のサイトに掲載してます下記記事の転載となります。

この記事が気に入ったらサポートをしてみませんか?