LoginSignup
51
65
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

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

Last updated at Posted at 2024-06-23

はじめに

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

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

Peek 2024-06-14 11-58.gif

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

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

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

動画で学びたい方へ

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

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

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

対象者

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

1. 環境構築

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

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を利用して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にアクセスすると以下の画面が表示されます

image.png

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

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

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

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

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

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

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

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

image.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
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

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

          <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を回して

を作ることでTODOを表示しています

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

 <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を追加できるようにしていきましょう

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

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

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

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

フォームに注目します

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

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

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

          <div>{todo}</div>

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

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を使って更新する必要があるので新たに用意します

  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はセットされているものが変更されたかをみています
セットされたリストの中身が変更されているかはみていないのです

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

Peek 2024-06-14 17-58.gif

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

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

削除機能

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

            {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));
                  }}

Peek 2024-06-14 18-05.gif

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

$ npm i react-icons

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

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

いい感じになりました

Firebaseへのデプロイ

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

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

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

image.png

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

image.png

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

image.png

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

image.png

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

image.png

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

image.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

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

おわりに

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

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

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

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

JISOUのメンバー募集中!

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

51
65
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
65