[TypeScript × React] Todoアプリ


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

デモはこちら　

https://stackblitz.com/edit/react-ts-dtcb55?embed=1&file=index.tsx


※以下は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ファイルを作成して開発を進めることにします。

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

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

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




# 3.todoアプリ開発

#### Types.ts で型定義

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

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

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


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

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


##### TaskItem.tsxで単体タスクコンポーネントを作成


```react:/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で、```handleDone```と```handleDelete```は関数なのでtypeに沿った書き方で定義します。
余談ですが、一応TypeScriptの型定義を「補足」に紹介しているので知りたい人はぜひ。

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




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

#### TaskList.tsxでTaskItemをマッピング

```react:/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で新規追加フォームの作成

```react:/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でコンポーネントを結合

```react:/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の型一覧

```typescript
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
```

で構築完了