1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[TypeScript × React] Todoアプリ

Last updated at Posted at 2021-09-04

TypeScriptで何か作ろうと思い立ち試しにReactとTypeScriptでTodoアプリを作ってみることにしました。

デモはこちら 

※以下はcreate-react-app環境下での内容になります。

この記事では以下を紹介します。

1.React開発環境インストール
2.todoアプリ設計
3.todoアプリ開発
補足

1.React開発環境インストール

React開発環境のインストール

% cd desktop
% npx create-react-app new-todo-ts --template typescript
% cd new-todo-ts
% npm start

でアプリをたちあげる。

TSのグローバル環境は下記「補足」の「TypeScriptグローバル環境構築」で紹介しています。

「missing script: start」の対処法

実行ディレクトリにpakage.jsonがそもそもないか、あっても scripts:{}の中にstartスクリプトがないことが原因。
カレントディレクトリにpakage.jsonがあるか確認。そのjsonのscripts{}にstartがなければ

"start": "react-scripts start"

を追記して再度実行してみる。

2.todoアプリ設計

開発にあたり簡単に設計を紹介。

基本的に/src配下にcomponentsディレクトリを作成→各コンポーネントは.tsxファイルを作成して開発を進めることにします。

作成ファイルを以下4つです。
・Types.ts => Taskの型定義
・TaskItem.tsx => 単体Taskのコンポーネント
・TaskList.tsx => TaskItem.tsxの親コンポーネント
・TaskInput.tsx => 新規Taskを追加するためのコンポーネント

作成ではありませんがデフォルトでApp.tsxがあります。こちらも初回Stateやreturnをいじります。
全部で作業ファイルは5つになります。

今回は上記紹介ファイル順に開発を進めていくことにします。

3.todoアプリ開発

Types.ts で型定義

まずはTypes.tsから。
このコンポーネントはTaskの型を定義します。
TypeScriptはtype(型)定義を主として開発を進めていくので変数・引数などにtypeを指定します。
下記に型定義のパターンがまとめられていたので参考までに。
https://www.tohoho-web.com/ex/typescript.html

todoアプリなので、それぞれのタスク単体にどんな値が入るかtype定義します。

はじめにsrc 配下にcomponentsディレクトリを作成し、Types.tsを作成。

/src/components/Types.ts
export type Task = {
    id: number,
    title: string,
    done: boolean
}

これでタスクのidとtitleとタスク状態を定義しました。
それぞれnumber / string / booleanと定義しました。
exportとしているのは別のファイルで呼び出すからです。
もちろん別ファイルに直書きもできますが、同じ内容ならexportで使用した方がメンテナンス的にもよろしいですね。

TaskItem.tsxで単体タスクコンポーネントを作成
/src/components/TaskItem.tsx
import React from 'react'
import { Task } from './Types'

type Props = {
    task: Task
    handleDone: (task: Task) => void
    handleDelete: (task: Task) => void
}

const TaskItem: React.FC<Props> = ({ task, handleDone, handleDelete }) => {
    return (
        <li className={ task.done ? 'done':'' }>
            <label>
                <input className="checkbox-input" type="checkbox" onClick={() => handleDone(task)} defaultChecked={task.done} />
                <span className="checkbox-label">{ task.title }</span>
            </label>
            <button className="delete-btn" onClick={ () => handleDelete(task)}>
                削除
            </button>
        </li>

    )
}

export default TaskItem

これ以降構造自体それほど変わらないのでここで詳細を説明します。
importでreactと最初に定義した{ Task }を呼び出します。
type Propsはこのコンポーネントで使用する値の型を定義します。
taskはimportしたTaskで、handleDonehandleDeleteは関数なのでtypeに沿った書き方で定義します。
余談ですが、一応TypeScriptの型定義を「補足」に紹介しているので知りたい人はぜひ。

さて続きですがReact.FC<Props>とすることで先に定義したPropsの型情報をセットしてtsxを記述できます。
こうすると引数に入る値それぞれがすっきりしますね。FCはFuction Componentの略です。
returnの中身をみると、
liタグにはtask.doneの状態で'done'というクラス名(ReactではclassをclassNameで記述)を分岐してます。

タスク単体のコンポーネントが作成できたので次はタスクリストを作成します。

TaskList.tsxでTaskItemをマッピング

/src/components/TaskList.tsx
import React from  'react'
import TaskItem from './TaskItem'
import { Task } from './Types'

type Props = {
    tasks: Task[]
    setTasks: React.Dispatch<React.SetStateAction<Task[]>>
}


const TaskList: React.FC<Props> = ({ tasks, setTasks }) =>  {
    const handleDone =  (task: Task) => {
        setTasks(prev => prev.map((t) => {
            if(t.id === task.id){
                return { ...task, done: !task.done }
            } else {
                return t
            }

        }))
    }

    const handleDelete = (task: Task) => {
        setTasks(prev => prev.filter((t) => {
            return t.id !== task.id
        }))
    }

    return (
        <div className="tasklist-container">
            {
                tasks.length <= 0 ? 
                'タスクの登録がありません'
                :
                <ul>
                    {
                        tasks.map(task => (
                            <TaskItem 
                              key={task.id}
                              task={task}
                              handleDone={handleDone}
                              handleDelete={handleDelete}
                            />
                        )) 
                    }
                </ul>
            } 
        </div>
    )
}

export default TaskList

TaskListなのでPropsはtasks:Task[]とします。
このコンポーネントでは関数処理に応じてListを更新しないといけません。setTasksではそのための型を定義しています。
初見だと意味不明だと思いますが、要は「Task[]という型を持つデータを定義して実行する関数」です。
この辺はReactがTypeScript用意している下記の型情報が参考になるので知りたい方はぜひ。
http://www.code-magagine.com/?p=13261

TaskListの中身ではdone関数とdelete関数を定義しています。
const handleDone = (task: Task) => {のtaskにはチェックしたtaskの情報が、setTasks(prev => prev.map((t) => {には現在あるtaskを一つずつマッピングしています。選択idで照合をかけて、もし該当すれば該当タスクのdoneプロパティの真偽値を変更します。
delete関数もクリックしたタスクのid以外をfilterをかけて表示(結果的に消えているように見える)しています。
これでタスクリストの作成ができました。

次に新規タスクを登録するinputフォームを作成します。

TaskInput.tsxで新規追加フォームの作成

/src/components/TaskInput.tsx
import React,{ useState } from 'react'
import  { Task } from './Types'

type Props = {
    setTasks: React.Dispatch<React.SetStateAction<Task[]>>
    tasks: Task[]
}

const TaskInput: React.FC<Props> = ({ setTasks,tasks }) => {
    const [inputTitle, setInputTitle] = useState<string>('')
    const [count, setCount] = useState<number>(tasks.length + 1)

    const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setInputTitle(e.target.value)
    }

    const handleSubmit = () => {
        setCount(count + 1)

        const newTask:Task = {
            id:count,
            title: inputTitle,
            done:false
        }

        setTasks([
            newTask, 
            ...tasks 
        ])

        setInputTitle('')
    }
    
    
    return (
        <div className="input-form">
                <div className="input-cotainer">
                    <input className="task-input" type="text" value={inputTitle} onChange={ handleInputChange } />
                    <button className="input-btn" onClick={ handleSubmit }  >
                        追加
                    </button>
                </div>
        </div>
    )
}

export default TaskInput

構造は従来と一緒ですが、useStateで型をそれぞれ定義・セットしています。
stringデータはconst [inputTitle, setInputTitle] = useState<string>('')
numberはconst [count, setCount] = useState<number>(tasks.length + 1)
という感じです。
この辺はReactのuseStateの理解が必要になりますが、要はstringではsetInputTitle経由でtitleにinputTitleをセットし、numberではidの値をsetCount経由でcountにセットしているという感じです。
作られた新しいタスクは const newTaskとしてsetTasksに追加処理がおこなわれます。以上の流れをhandleSubmitをクリックすることで一連の処理が行われます。
これでタスク追加のコンポーネントができました。

あとはrootディレクトリで存在するApp.tsxにそれぞれのファイルをimportしましょう。

デフォルトのApp.tsxでコンポーネントを結合

/src/App.tsx
import React,{ useState } from 'react'
import './App.css';
import { Task } from './components/Types'
import TaskInput from './components/TaskInput'
import TaskList from './components/TaskList'



const initialState: Task[] = [
  {
    id: 2,
    title: 'Hello World',
    done: false
  },
  {
    id: 1,
    title: 'Start React Todo App by TS',
    done: false
  }
]


const App:React.FC = () => {
const [tasks, setTasks] = useState(initialState)
  return (
    <div className="todo-container">
      <div className="todo-component">
        <TaskInput setTasks={setTasks} tasks={tasks} />
        <TaskList setTasks={setTasks} tasks={tasks} />
      </div>
    </div>
  );
}

export default App;

これでデモのTodoアプリができました。
Reactはより秀逸な書き方があとで追加されたりと割とアップデートが多いようなので、このあたりは追って更新する必要がありますね。TSも勉強することでより深く学べそうです。

補足

Propsの型一覧
type Props = {
  str: string;
  num: number;
  bool: boolean;
  obj: {
    str: string;
  };
  strArr: string[];
  objArr: {
    str: string;
  }[];
  func: () => void;
}
TypeScriptグローバル環境構築

これからtypescriptを学習したい方はグローバルにtypescriptをインストールする方法も紹介します。
下記はzshなので参考程度にどうぞ。(node が入っていることが前提)
ターミナルを開いて、

% npm i typescript -g
% tsc --version
// command not found なら以下
% npm bin -g
// /Users/xxx/.nvm/ ~ とか
% sudo vi ~/.zshrc

iでインサートモードに切り替えて
export PATH=$PATH:`npm bin -gを一番下に追記
保存したいので:wqで保存してvimモードから抜ける
pathを更新したいので、

% source ~/.zshrc

でpathを反映する

% tsc --version
// 4.4.2

で構築完了

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?