🦁

NextjsとTypeScriptで簡単なTodoアプリを作ろう!

2023/03/17に公開

はじめに

Nextjs と TypeScript で簡単な Todo アプリを作ってみました!
初心者でもわかるようになるべくわかりやすく解説していきます!

完成したデモサイトです。
デモサイト

見ての通りスタイルは一切当てていない todo リストですが
React,Nextjs ではよく用いられる構文が出てきますので
頑張っていきましょう。

準備

まずはプロジェクトの作成です。

// npmの場合
$ npx create-next-app --ts

// yarnの場合
$ yarn create next-app --typescript

フォルダが構成されましたら、
まず CSS を消します。

globals

全て消して大丈夫です。

次に pages フォルダにある index.tsx で余分なものを消します

index
export default function Home() {
  return (
    <>
    <main></main>
    </>
  );
}

Todo の作成

まずは Todo を入力する為の Input とボタンを配置します。

index
export default function Home() {
  return (
    <>
      <main>
+       <div>
+          <input type="text" />
+          <button>追加</button>
+      </div>
      </main>
    </>
  );
}

インプットで入力した文字を反映させます。

index
+   import { useState } from "react";

export default function Home() {
+   const [text,setText] = useState<string>('')

+   const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
+    setText(e.target.value);
+    console.log(text)
+  };

  return (
    <>
      <main>
        <div>
+            <input type="text" value={text} onChange={changeText} />
            <button>追加</button>
       </div>
      </main>
    </>
  );
}

急に色々と増えましたね、1つずつ見てみましょう。

index
const [text,setText] = useState<string>('')

まずこちらですが、 useState を用いて text という状態と
setText という関数を string 型で初期値は空で定義しています。

そもそも useState とは?

React において、簡単に状態を管理するための Hooks の 1 つで、
state とは内部で保持する状態のことです。
コンポーネント内で状態管理を行いたい変数を宣言し、
2 つの値がペアで返されます。
第一引数に状態を管理したい変数を定義します。
第二引数は第一引数を更新するためのものでこの関数を呼び出すことで
第一関数の値が更新されます。

index
 const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

こちらは changeText という関数を定義しています。
この関数は e を引数として受け取りその e から target を取り出し、
さらにその value を取り出して setText 関数に渡しています。
e の型は input 要素で何らかの変更が加えられた時の方です。
onClick イベントだと React.MouseEvent<HTMLInputElement>になります。

index
<input type="text" value={text} onChange={changeText} />

この input 要素はテキストを入力すると onChange イベントが発生し
先ほどの changeText 関数が呼び出されます。
value の値が text にバインドされている為 text の値は
入力されたテキストに更新されます。

続いてボタンを押した際に Todo を追加する関数を追加します。

index
export default function Home() {
    const [text, setText] = useState<string>("");
+   const [todos, setTodos] = useState<string[]>([]);

    const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
    };

+ const addTodos = () => {
+    const newTodos = [...todos];
+    newTodos.push(text);
+    setTodos(newTodos);
+    setText("");
+  };

return (

今回も1つ1つ解説します。

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

今度は todos を useState を用いて状態管理します。
todo リストはいくつもあることを想定して複数形で
配列として設定します。こちらも初期値は空です。

index
const addTodos = () => {
    const newTodos = [...todos];
    newTodos.push(text)
    setTodos(newTodos);
    setText("");
}

こちらは追加ボタンを押した際に新しい Todo が追加される addTodos 関数です。
細かく解説していきます。

index
const newTodos = [...todos];

まず newTodos 関数を作成し todos をスプレット構文で全てを取り出します。

スプレット構文とは?

配列やオブジェクトを展開して、その中身を別の配列やオブジェクトに展開するための構文です。
スプレット構文を用いることにより、元の todos 配列を変更することなく、
新しい要素を追加することができます。

index
newTodos.push(text)

そして newTodos 関数に対して要素を追加する push メソッドを用いて
テキスト入力した値を追加します。

index
setTodos(newTodos);

テキスト入力した値を追加した newTodos を setTodos を通じて
todos に上書きします、その結果画面上に表示される TODO リストが更新されます。

index
setText("");

最後に setText に空要素を渡してあげることにより、
input に入力された値が空になります。
これをやらないと追加ボタンを押しても input に入力された値は
そのまま残ります。

Todo を追加する関数を作成したらクリックイベントとして
ボタンに addTodos を追加します。

index
return (
    <>
        <main>
            <div>
                <input type="text" value={text} onChange={changeText} />
-               <button>追加</button>
+               <button onClick={addTodos}>追加</button>
            </div>
        </main>
    </>
)

これで input にテキストを入力して追加ボタンを押すと
TODO が追加されるようになりました。
画面に表示される要素がないので追加していきます。

index
return (
     <main>
      <div>
        <input type="text" value={text} onChange={changeText} />
        <button onClick={addTodos}>追加</button>
      </div>
+      <div>
+        <ul>
+          {todos.map((todo) => (
+            <li key={todo}>
+              <p>{todo}</p>
+              <button>完了</button>
+            </li>
+          ))}
+        </ul>
+      </div>
    </main>
)

useState で状態管理している todos を map 関数を用いて
展開しています。

map 関数とは?

配列の各要素に同じ処理をして新しい配列を生成するための関数です
第一引数に繰り返したい要素、第二引数はその番号を振り分ける index を指定できます。
React がどの要素が変更、削除、追加されたのかを識別するため
親要素には key が必要で被らない要素を key として選ぶと最適です。

p タグに map 関数で展開した todo を入れます。
こうすることにより、input で入力したテキストが p タグで出力されます。

これで入力した値が表示されるようになりました。

最後に完了ボタンを押した時に TODO を削除する関数を作成します。

index
const addTodos = () => {
    const newTodos = [...todos];
    newTodos.push(text);
    setTodos(newTodos);
    setText("");
  };

+  const deleteTodo = (index: number) => {
+    const newTodos = [...todos];
+    newTodos.splice(index, 1);
+    setTodos(newTodos);
+  };

  return (

deleteTodo という関数名で引数に index で namber 型を指定しています。
1つ1つ解説していきます。

index
const newTodos = [...todos];

今回も newTodos としてスプレット構文を用いて todos を全て取り出します。

index
newTodos.splice(index, 1);

newTodos に対して splice メソッドを用いて完了ボタンを
クリックした要素を削除します。
第一引数削除する要素を指定するので、
ここでは完了ボタンを押した TODO として index を指定します。
第二引数に削除する数を指定できます。今回は1つです。

index
 setTodos(newTodos);

最後に todos を更新するための要素を書きます。

完了ボタンに対して、onClick イベントで deleteTodo を呼び出します。

index
return (
    <main>
      <div>
        <input type="text" value={text} onChange={changeText} />
        <button onClick={addTodos}>追加</button>
      </div>
      <div>
        <ul>
-          {todos.map((todo) => (
+          {todos.map((todo, index) => (
            <li key={todo}>
              <p>{todo}</p>
-              <button>完了</button>
+              <button onClick={() => deleteTodo(index)}>完了</button>
            </li>
          ))}
        </ul>
      </div>
    </main>
  );

map 関数の第二引数に index を指定し、todo 要素に番号を振り当てます。
button に対して無名関数をコールバック関数として deleteTodo 関数を渡しています。
こうすることによりボタンをクリックされた時に deleteTodo 関数が実行されます。

index
<button onClick={deleteTodo(index)}>完了</button>

ちなみにこの書き方だと deleteTodo 関数がコールバック関数として即時に
実行されてしまい、ボタンがレンダリングされると同時に
deleteTodo 関数が実行されてしまいます。

コードの全体です

index.tsx

import { NextPage } from "next";
import { useState } from "react";

const Home: NextPage = () => {
  const [text, setText] = useState<string>("");
  const [todos, setTodos] = useState<string[]>([]);

  const changeText = (e: React.ChangeEvent<HTMLInputElement>) => {
    setText(e.target.value);
  };

  const addTodos = () => {
    const newTodos = [...todos];
    newTodos.push(text);
    setTodos(newTodos);
    setText("");
  };

  const deleteTodo = (index: number) => {
    const newTodos = [...todos];
    newTodos.splice(index, 1);
    setTodos(newTodos);
  };

  return (
    <main>
      <div>
        <input type="text" value={text} onChange={changeText} />
        <button onClick={addTodos}>追加</button>
      </div>
      <div>
        <ul>
          {todos.map((todo, index) => (
            <li key={index}>
              <p>{todo}</p>
              <button onClick={() => deleteTodo(index)}>完了</button>
            </li>
          ))}
        </ul>
      </div>
    </main>
  );
};

export default Home;

さいごに

お疲れ様でした!これで Todo リストの完成です!

最後までお読みくださりありがとうございます。
今回の Todo はスタイルが一切当てられていないので、TailwindCSS や
Chakra ui を用いてスタイルを当ててみたり、今 input が空白でも Todo リストに
追加できてしまうので、それを止めるプログラムを書いてみたり色々と挑戦してみてください!

Discussion