【初心者ハンズオン】ReactでTODOアプリを作ってデプロイしよう【Firebase/Shadcn/TailwindCSS】

# はじめに

今回のハンズオンではReactをこれからやっていきたい人に向けてTODOアプリを作りながらReactの基本的な使い方から実際にアプリケーションをデプロイしてユーザーが使えるリリースまでを行えます。

アプリケーションはとてもシンプルなものですので、短時間で学習することが可能です

![Peek 2024-06-14 11-58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/a71b303b-7284-df75-2d20-6966bd3f6f72.gif)

初心者向けに本記事は書いておりますので、丁寧にセットアップから紹介していきます。
JavaScriptなどの前提知識は不要で、HTMLとCSSの知識があれば行っていただけます

またこのハンズオンでは最近人気がでている**Shadcn/ui**を活用しており、学習した後に簡単にカスタマイズをしていただけるはずです。

**ハンズオンだけでは使える実力はつきません。ぜひともここで習ったことを活かしながら、似たようなアプリケーションを作成してみてください**

# 動画で学びたい方へ

こちらのハンズオンはYoutubeの動画でも公開しております
より詳しく説明を聞きたい方はぜひご覧ください

https://youtu.be/LrwTuMTyxJM?si=bb2ys0STGwahdboa

# 本ハンズオンで学べること

- JavaScriptの基本的な構文
- Reactの使い方 (useState, イベント)
- Shadcn/uiの使い方
- TailwindCSSの使い方
- Firebaseのデプロイ方法
- react-iconsの利用方法

# 対象者

- HTML/CSSの経験がある
- Reactに興味があるが触ったことがない
- 手を動かして学びたい
- デプロイをしたことがない

# 1. 環境構築

まずはReact/TypeScript/Shadcn(TawilwindCSS)の環境から作ります。
PCのターミナルを開いてまずは`node`と`npm`をインストールします

Windowsの方
https://prog-8.com/docs/nodejs-env-win

Macの方

```
$ brew install node
```

Ubuntuの方

```
$ sudo apt install -y nodejs npm
```

インストールできたら確認をします

```
$ npm -v
Node.js v18.17.0

$ node -v
```

次に[Vite](https://ja.vitejs.dev/)を利用してReact環境を作ります

```
$ 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
```

[localhost:5173](http://localhost:5173)にアクセスすると以下の画面が表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/390fdb5f-739c-becb-fe09-9239be020fba.png)

次にShadcnを導入します
別のターミナルを開いて`first-todo`ディレクトリに移動してインストールします

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

first-todoディレクトリをVSCodeで開いてください

`tsconfig.json`を変更します

```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" }]
}
```

`vite.config.ts`を変更します

```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
```

`src/App.tsx`を以下にします

```tsx:Ap.tsx
import { Button } from "@/components/ui/button"

export default function Home() {
  return (
    <div>
      <Button>Click me</Button>
    </div>
  )
}
```

[localhost:5173](http://localhost:5173)をみると黒いボタンが表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/9384179f-a6c4-1d1b-ca52-625552feca30.png)

shadcnの導入ができました！

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

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

shadcnのCardとInputを利用します

https://ui.shadcn.com/docs/components/card
https://ui.shadcn.com/docs/components/input

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


```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";

export default function Home() {
  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>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/48d1c329-d066-414c-313f-27b21c19ef83.png)

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

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

このようにHTMLのなかでmapを回して<li>を作ることでTODOを表示しています

スタイリングにはTailwindCSSを使っています

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

このようにcssをクラスで当てることができます

- bg-gray-100 : 背景色
- flex : display : フレックスにする
- justiyf-center : 要素は右左で中心に配置
- items-center : 要素を上下で中心に配置

このようにCSSを細かく当てなくてもできるのがTailwindCSSです

`<Card>`など大文字で始まるタグは**Shadcn**のコンポーネントで、タグを利用することで簡単におしゃれなデザインが利用可能です

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

ここからはReactの内容になっていきます
まずはTODOを追加できるようにしていきましょう

```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";

export default function Home() {
  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> //追加
          <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>
  );
}
```

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

![Peek 2024-06-14 17-45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/89e16b30-2b8f-7c0a-79e7-ec538a2b84a8.gif)

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

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

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

フォームに注目します

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

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

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

```tsx
          <div>{todo}</div>
```

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

```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";

export default function Home() {
  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>
  );
}
```

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

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

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

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

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

ボタンに`onClick`を追加します

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

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

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

このようにいま表示されているtodosの後ろにtodoを追加して更新しています

todosが["A" "B"]でCを追加したとしたらsetTodos(["A", "B", "C"])と同じになります

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

```tsx
setTodos(todos.push(todo));
```

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

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

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

![Peek 2024-06-14 17-58.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/71b506ef-c22d-20e3-0b23-3c4c81966a80.gif)

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

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

# 削除機能

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

```tsx
            {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か)をわかるようにします

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

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

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

![Peek 2024-06-14 18-05.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/34b14146-fecf-1e1e-b938-70dc21280c70.gif)

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

```
$ npm i react-icons
```

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

```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"; //追加

export default function Home() {
  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]);
              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>
  );
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/ae72af6c-14c3-d6ac-c20d-8162704d562f.png)

いい感じになりました

# Firebaseへのデプロイ

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

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

https://firebase.google.com/?hl=ja

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/62a5c38f-b03f-07f9-70b1-6066357e3fca.png)


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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/557a8ae3-ae9e-771f-95d3-314d847d7dc8.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/c9d8a7fe-1fbf-a9d4-da8e-c386639b3040.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/87e0ee62-2fe7-e91a-cbec-1d0f2757029a.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/254e9fc6-7e3d-465a-f8b2-0ec72fa2ebeb.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/7b8a486d-54d9-8957-6a3f-292dabce3134.png)

ここまでできたらターミナルに戻って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) 
ここで必ずエンターを押してください

? In which region would you like to host server-side content, if applicable? asia-east1 (Taiwan)
台湾を選択

? Set up automatic builds and deploys with GitHub? (y/N) N
Noを選択
```

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

```
$ 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が発行されるのでアクセスします

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/810513/e988f6d4-1d99-2a85-2a69-252d8df1aef2.png)

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

# おわりに

今回はReactの基本を説明するためにTODOアプリを作りました
実際にユーザーに価値を届けるためにはデプロイも大切なので最後に行いました

このハンズオンでは細かいReactやJavaScriptの説明は省略していますので、気になる方はぜひ動画をみて作ってみてください

https://youtu.be/LrwTuMTyxJM?si=bb2ys0STGwahdboa

ここまで読んでいただけた方はいいねとストックよろしくお願いします。
@Sicut_study をフォローいただけるととてもうれしく思います。

また明日の記事でお会いしましょう！

# JISOUのメンバー募集中！
プログラミングコーチングJISOUでは、新たなメンバーを募集しています。
実践的なカリキュラムで、あなたのエンジニアとしてのキャリアを最短で飛躍させましょう！
興味のある方は、ぜひホームページからお気軽にカウンセリングをお申し込みください！
▼▼▼

https://projisou.jp