ReactとTypescriptでTODOアプリを言語化しながら作ってみる

komosyu
React

目次

  1. はじめに
  2. さっそく実装してみる
    1. 条件
    2. 全体の流れをざっくり掴む
    3. 1.React プロジェクトの作成
    4. 2.各コンポーネント作成
    5. 3.ロジックを実装する
      1. 値をグローバルで管理する
      2. TODO 全体の管理
      3. TODO を追加するためのフォーム
      4. TODO を表示するためのリスト
      5. それぞれの TODO を管理
  3. 参考
  4. さいごに

はじめに

Next.js や Astro といったフレームワークで Jamstack サイトの実装をするのは好きなのですが、React に関しての知識は正しく理解できていなかったりした気がしたので、改めて React の初歩の初歩である TODO アプリを TypeScript で実装してみることとしました。

それと、これまで存在は知っていたけれど実装の際に使う機会のなかったuseReduceruseContextを組み込んでいこうと思います!

(Udemyで勉強したことをアウトプットしたいだけではありますが。。)

さっそく実装してみる

それでは、さっそく実装を!と言いたいところですが、その前に条件と簡単に実装の流れを整理してみましょう。

条件

  • 追加、更新、削除が可能
  • useReduceruseContext を使用する
  • リロード時には消えてしまって ok(db や localstrage は使わない)

全体の流れをざっくり掴む

  • 1.React プロジェクトの作成
  • 2.各コンポーネントとコンテキスト作成
  • 3.ロジックを実装する

1.React プロジェクトの作成

ふだんプロジェクトを作成するような適当なディレクトリ内に CRA(create-react-app)をしていきます。
ターミナルで以下のコマンドを叩きます。

npx create-react-app --template typescript todo-app-ts

todo-app-ts には適当にプロジェクト名を入れてください。

その後、作成したプロジェクト内に移動して

cd todo-app-ts

アプリを起動していきます。

npm start

コマンドを入力するとhttp://localhost:3000といった感じでローカル環境を立ち上げることができます。

これでプロジェクトを作成するための下準備が整いましたので、これから各コンポーネントでの実装方法をみていきたいと思います。

2.各コンポーネント作成

それではまずは各コンポーネントを作成します。

TODO アプリを実装するにあたって、以下の機能が必要になるので、それらを実現するためのコンポーネントを実装していこうと思います。

  • 値をグローバルで管理する(/context/TodoContext.tsx)
  • TODO 全体の管理(/components/Todo.tsx)
  • TODO を追加するためのフォーム(/components/TodoForm.tsx)
  • TODO を表示するためのリスト(/components/TodoList.tsx)
  • それぞれの TODO を管理(/components/TodoItem.tsx)

3.ロジックを実装する

コンポーネントの準備が整ったので、それぞれのロジックを作成してきましょう!

値をグローバルで管理する

今回はuseReduceruseContext使用してグローバルに値を管理するということで、そちらの設定から進めていきます。

import { useContext, createContext, useReducer } from 'react'

// TODOリストの状態を保持するContextを作成
const TodoContext = createContext()
// Todoリストの状態を更新するためのDispatch関数を保持するContextを作成
const TodoDispatchContext = createContext()

// 初期のTODOリストのデータを用意しておく
const todosList = [
  { id: 1, content: 'a', editing: false },
  { id: 2, content: 'b', editing: false },
  { id: 3, content: 'c', editing: false },
]

// Reducer関数の定義
const todoReducer = (todos, action) => {
  // 追加、更新、削除によって処理を分ける
  switch (action.type) {
    // 新しいTODOを追加する場合
    case 'todo/add':
      return [...todos, action.todo]
    // TODOを削除する場合
    case 'todo/delete':
      return todos.filter((todo) => todo.id !== action.todo.id)
    //  TODOを削除する場合
    case 'todo/update':
      return todos.map((_todo) =>
        _todo.id === action.todo.id
          ? { ..._todo, ...action.todo }
          : { ..._todo }
      )
    // それ以外
    default:
      return todos
  }
}

// TODOの状態とReducerを結びつけるProviderを用意
const TodoProvider = ({ children }) => {
  // useReducerフックでtodoReducer関数を使ってtodosListを初期値として状態を管理し、dispatch関数を取得する
  const [todos, dispatch] = useReducer(todoReducer, todosList)
  return (
    // TodoContext.ProviderとTodoDispatchContext.Providerでtodosとdispatchを子コンポーネントに渡す
    <TodoContext.Provider value={todos}>
      <TodoDispatchContext.Provider value={dispatch}>
        {children}
      </TodoDispatchContext.Provider>
    </TodoContext.Provider>
  )
}

// Todoリストの状態を取得するカスタムフックを作成
const useTodos = () => useContext(TodoContext)
// dispatch関数を取得するカスタムフック
const useTodoDispatch = () => useContext(TodoDispatchContext)

export { useTodos, useTodoDispatch, TodoProvider }

TODO 全体の管理

それでは表示部分の実装に移っていきましょう!

import React from 'react'
import { TodoProvider } from '../context/TodoContext'
import { TodoList } from './TodoList'
import { TodoForm } from './TodoForm'

export const Todo = () => {
  return (
    // 先ほど作成した`Provider`を使って全体を囲ってあげることで状態を管理してあげます
    <TodoProvider>
      <TodoList />
      <TodoForm />
    </TodoProvider>
  )
}

TODO を追加するためのフォーム

入力内容を TODO リストに追加するためのフォームを実装していきます!

import { useState } from 'react'
import { useTodoDispatch } from '../context/TodoContext'

type Todo = {
  id: number,
  content: string,
}

type TodoFormProps = {
  createTodo: (todo: Todo) => void,
}

export const TodoForm: React.FC<TodoFormProps> = ({ createTodo }) => {
  // 入力内容を管理
  const [enteredTodo, setEnteredTodo] = useState('')
  // Todoアイテムを追加するためのdispatch関数を取得
  const dispatch = useTodoDispatch()
  // 新たに追加されたTODOのidを設定
  const newTodoId = useTodos().length + 1
  // 入力内容を更新していく
  const onChangeValue = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEnteredTodo(e.target.value)
  }
  // フォームのSubmitイベントによってTODOを追加
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    // 新しく追加するTODO
    const newTodo = {
      id: newTodoId,
      content: enteredTodo,
    }
    // ここでは追加したいので'todo/add'で指定する
    dispatch({ type: 'todo/add', todo: newTodo })
    // 入力内容を初期化
    setEnteredTodo('')
  }

  return (
    <form onSubmit={onSubmit}>
      <input type="text" value={enteredTodo} onChange={onChangeValue} />
    </form>
  )
}

TODO を表示するためのリスト

入力された TODO を管理するリストを作成していきます!

import { useTodos } from '../context/TodoContext'
import { TodoItem } from './TodoItem'

type Todo = {
  id: string,
  content: string,
}

export const TodoList = () => {
  // useTodosで現在のTODOを取得してくる
  const todos: Todo[] = useTodos()
  return (
    <div>
      <ul>
        {todos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  )
}

それぞれの TODO を管理

それぞれの TODO を表示していきます!

ここでは、それぞれで更新と削除ができるようにしていきます。

import { useState } from 'react'
import { useTodoDispatch } from '../context/TodoContext'

type Props = {
  id: string,
  content: string
}

export const TodoItem:React.FC<Props>  = ({ todo }) => {
  // 更新された入力内容を管理
  const [editingContent, setEditingContent] = useState(todo.content)
  // Todoアイテムを更新・削除するためのdispatch関数を取得
  const dispatch = useTodoDispatch()
  // Todoアイテムを更新していく
  const changeContent = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEditingContent(e.target.value)
  }
  // Todoアイテムの更新状態を管理する
  const toggleEdditMode = () => {
    const newTodo = { ...todo, editing: !todo.editing }
    // ここでは更新したいので'todo/update'で指定する
    dispatch({ type: 'todo/update', todo: newTodo })
  }
  // 更新したした内容を確定する
  const confirmContent = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault()
    const newTodo = { ...todo, content: editingContent, editing: !todo.editing }
    // ここでは更新したいので'todo/update'で指定する
    dispatch({ type: 'todo/update', todo: newTodo })
  }
  // TODOを削除する
  const delete = (todo: Todo) => {
    // ここでは削除したいので'todo/delete'で指定する
    dispatch({ type: 'todo/delete', todo })
  }

  return (
    <li>
      <form onSubmit={confirmContent}>
        <span className="">
          {todo.editing ? (
            <input
              type="text"
              value={editingContent}
              onChange={changeContent}
            />
          ) : (
            <span onDoubleClick={toggleEdditMode} className="">
              {' '}
              {todo.content}
            </span>
          )}
        </span>
      </form>
      <button onClick={() => delete(todo)} className="">
        削除
      </button>
    </li>
  )
}

参考

さいごに

また Udemy での学びをアウトプットしただけの感じになってしまいましたが、React と TypeScript での TODO アプリの実装は転職活動での技術課題とかでもよく出てくるものなので、実装の流れは理解しておいた方が良いでしょう!

useReduceruseContextも使っていたら、できる人っぽく見えるかもしれないので、今回のパターンも抑えておくとよいかもしれませんね。