初めに
React & TypeScriptを通しての、todoアプリの作り方を紹介します。
とても基礎的なアプリなので、実際にコードを読んで試してみて、参考にしてください!
対象読者:reactを勉強し始めて浅い方(勉強した事がある人が対象です!)
初めて、reactで簡単なアプリを作ってみよう!と考えた方におすすめです
向かない方:reactを勉強して長い方 & 「react&JavaScript」を勉強した事がない方
環境&構築
node.js等が入っていない人は、こちらの記事を参考にお願いします。
https://clover-weblog.com/nextjs-start/
手順
環境が構築が済んだら、以下のコマンドで作成
npx create-next-app todos-app --typescript
package.jsonは以下のようなバージョンで作成されました(2023/01/02時点)

私の環境
❯ node -v             
v16.14.2
% npx -v                                    
8.19.2
"react": "^18.2.0"
- react18系
- node.js 16.14.2
- 
npx create-next-app todos-app --typescriptで作成
補足
この記事に興味を持ちましたら、以下のハンズオンをやることをオススメします。
reactでtodoを学ぶなら本当におすすめのzennの記事
解説も素晴らしく、本当に良い記事だと思います
基本のtodoアプリ作成方法
import { useState } from 'react';
export default function index() {
  const [text, setText] = useState<string>("");
  const [todos, setTodos] = useState<string[]>([]);
  // 送信されてきた値をstateに保存
  function onChange(e:React.ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }
  // 送信ボタンを押した後、入力フォームに書かれている値をtodosのstateに保存
  function onSubmit(e:React.FormEvent<HTMLFormElement>) {
    // 送信ボタンを押した後の画面遷移を防ぐ
    e.preventDefault();
    // 新しくtodosのstateを更新するための、仮の配列
    const newTodos = [...todos];
    // 入力フォームに書かれている値を追加する
    newTodos.push(text);
    setTodos(newTodos);
    setText("");
  }
  
  return (
    <div>
      <h1>todos</h1>
       {/* リロードを防ぐために、イベントオブジェクトを渡す */}
      <form onSubmit={(e) => {onSubmit(e)}}>
        <input type="text" value={text} onChange={onChange} />
        <button type='submit' onSubmit={() => onSubmit}>送信</button>
      </form>
      <div>
        <ul>
          {/* 登録されているtodosを展開する */}
          {todos.map((todo,index) => {
            // mapで展開する値には、keyプロパティを指定して、ユニークな値を渡す(reactが、一つ一つのエレメントを識別するために)
            return <li key={index}>{todo}</li>
          })}
        </ul>
      </div>
    </div>
  )
}
削除ボタンも追加する
削除ボタンも追加する場合は、以下のように書きます
import { useState } from 'react';
// todoの型定義。mapで特定するためにidも付与
type Todos = {
  id:number,
  text:string
}
export default function index() {
  const [text, setText] = useState<string>("");
  // todoは複数あるので、配列で作成
  const [todos, setTodos] = useState<Todos[]>([]);
  // 送信されてきた値をstateに保存
  function onChange(e:React.ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }
  function onSubmit(e:React.FormEvent<HTMLFormElement>) {
    // 送信ボタンを押した後の画面遷移を防ぐ
    e.preventDefault();
    // todosに保存するための、新しいtodoを作成
    const newTodo: Todos = {
      id: new Date().getTime(), // ユニークな値なら何でも良い
      text: text // stateのtext(入力フォームに書かれている値)を保存
    }
    // todosを新しく作り直すので、既存のtodosに、newTodoを足す
    const newTodos = [...todos,newTodo];
    setTodos(newTodos); // 更新
    setText("");
  }
  function onDelete(id:number) {
    // 削除するボタンを押した後、指定されたtodoを消すのだが、その前に、完全に別物のtodosのコピーを作成する必要がある
    // オブジェクト型をコピーすると、通常は参照先をコピーしてしまうので、そうならないようにするmapの戻り値で新しい配列を作成する。
    // こちらの、「shallow copyとdeep copy」の記事を読むと、オブジェクト型がどのようなコピーをするのかが分かると思いますhttps://www.wakuwakubank.com/posts/744-javascript-object-copy/
    const deepCopy = todos.map((todo) => ({ ...todo }));
    deepCopy.map((todo,index) => {
      if (todo.id === id) {
        // 既存のtodoのidと、引数で渡ってきたidが同じだったら、spliceメソッドで削除する
        deepCopy.splice(index,1)
      }
    });
    // 先程のdeepCopyを渡す事で、stateが更新されて削除される
    setTodos(deepCopy);
  }
  
  return (
    <div>
      <h1>todos</h1>
      {/* リロードを防ぐために、イベントオブジェクトを渡す */}
      <form onSubmit={(e) => {onSubmit(e)}}>
        {/* 入力された値をstateに保存する */}
        <input type="text" value={text} onChange={onChange} />
        {/* 送信ボタンを押したら、todosに新しいtodoを追加する */}
        <button type='submit' onClick={() => onSubmit}>送信</button>
      </form>
      <div>
        <ul>
          {/* 登録されているtodosを展開する */}
          {todos.map((todo) => {
            return <li key={todo.id}>
                    {todo.text} 
                    {/* 削除ボタンを押した時に、todoのidを渡す事で特定出来るようにし、spliceメソッドで削除する */}
                    <button type="submit" onClick={() => onDelete(todo.id)}>削除</button>
                   </li>
          })}
        </ul>
      </div>
    </div>
  )
}
完了した場合、チェックを付けると、打ち消し線が付くようにする
ディレクトリ構成
import { useState } from 'react';
// チェックボックス入力後、cssで打ち消し線を実装したいため使用
import classes from "../styles/Index.module.css"
// todoの型定義。mapで特定するためにidも付与
type Todos = {
  id:number,
  text:string,
  isDone:boolean
}
export default function index() {
  const [text, setText] = useState<string>("");
  // todoは複数あるので、配列で作成
  const [todos, setTodos] = useState<Todos[]>([]);
  // 送信されてきた値をstateに保存
  function onChange(e:React.ChangeEvent<HTMLInputElement>) {
    setText(e.target.value);
  }
  function onSubmit(e:React.FormEvent<HTMLFormElement>) {
    // 送信ボタンを押した後の画面遷移を防ぐ
    e.preventDefault();
    // todosに保存するための、新しいtodoを作成
    const newTodo: Todos = {
      id: new Date().getTime(), // ユニークな値なら何でも良い
      text: text, // stateのtext(入力フォームに書かれている値)を保存
      isDone:false
    }
    // todosを新しく作り直すので、既存のtodosに、newTodoを足す
    const newTodos = [...todos,newTodo];
    setTodos(newTodos); // 更新
    setText("");
  }
  function onDelete(id:number) {
    // 削除するボタンを押した後、指定されたtodoを消すのだが、その前に、完全に別物のtodosのコピーを作成する必要がある
    // オブジェクト型をコピーすると、通常は参照先をコピーしてしまうので、そうならないようにするmapの戻り値で新しい配列を作成する。
    // こちらの、「shallow copyとdeep copy」の記事を読むと、オブジェクト型がどのようなコピーをするのかが分かると思いますhttps://www.wakuwakubank.com/posts/744-javascript-object-copy/
    const deepCopy = todos.map((todo) => ({ ...todo }));
    deepCopy.map((todo,index) => {
      if (todo.id === id) {
        // 既存のtodoのidと、引数で渡ってきたidが同じだったら、spliceメソッドで削除する
        deepCopy.splice(index,1)
      }
    });
    // 先程のdeepCopyを渡す事で、stateが更新されて削除される
    setTodos(deepCopy);
  }
  function onCheck(id:number) {
    const deepCopy = todos.map((todo) => ({ ...todo }));
    const newTodos = deepCopy.map((todo) => {
      if (todo.id === id) {
        // クリックする度に、booleanの値を反転する(!で)
        todo.isDone = !todo.isDone;
      }
      return todo
    });
    // boolean反転させた結果で、再作成
    setTodos(newTodos);
  }
  
  return (
    <div>
      <h1>todos</h1>
      {/* リロードを防ぐために、イベントオブジェクトを渡す */}
      <form onSubmit={(e) => {onSubmit(e)}}>
        {/* 入力された値をstateに保存する */}
        <input type="text" value={text} onChange={onChange} />
        {/* 送信ボタンを押したら、todosに新しいtodoを追加する */}
        <button type='submit' onClick={() => onSubmit}>送信</button>
      </form>
      <div>
        <ul>
          {/* 登録されているtodosを展開する */}
          {todos.map((todo) => {
            return <li key={todo.id} className={todo.isDone ? classes.done : ""}>
                    <input type="checkbox" onClick={() => onCheck(todo.id)} />
                    {todo.text} 
                    {/* 削除ボタンを押した時に、todoのidを渡す事で特定出来るようにし、spliceメソッドで削除する */}
                    <button type="submit" onClick={() => onDelete(todo.id)}>削除</button>
                   </li>
          })}
        </ul>
      </div>
    </div>
  )
}
.done{
  text-decoration: line-through;
}
参考文献
めちゃくちゃオススメです!
React & TypeScriptを学ぶのに最適です!
初心者に本当におすすめ
じゃけぇ本
めちゃくちゃわかりやすいtodoアプリの作り方。
参考にさせていただきました(1000円払わなくても、途中までは出来ます)
reactでtodoを学ぶなら本当におすすめのzennの記事
udemyで超おすすめのjs講座
udemyで超おすすめのreact講座
js自体をちゃんと学ぶのも大事だと思うので、この本をオススメします
今後
色々消化不良なので、もう少し色々修正して記事を出します



