環境構築
node.jsは入っていることとします。今回はcreate-react-appで始めます。
$ npx create-react-app todo-ts --typescriptインストールが終わったら。下記を実行します。
$ cd todo-ts$ npm startブラウザが起動して何か表示されたら準備完了!
完成イメージ
今回作成するTodoアプリのイメージです。
この規模だとコンポーネント分けると逆に面倒そうですが、練習も兼ねて分けてみましょう。
文字を入力するTaskInput.tsxと一覧を表示するTaskList.tsx、一覧の一つになるTaskItem.tsxを作ります。

CSS
CSSをざっと作ります。
Reactはライブラリとか使って、コンポーネント毎にスタイルを作った方が良いと思いますが、今回は全部App.cssに入れます。
*, *:active, *:hover, *:before, *:after { box-sizing: border-box;}
.inner { max-width: 700px; margin: 0 auto;}
.task-list { list-style: none; margin: 0; padding: 0; border-top: 1px solid #d7d2cd;}.task-list li { padding: 20px 10px; border-bottom: 1px solid #d7d2cd; display: flex; align-items: center; position: relative;}
.task-list li.done span { text-decoration: line-through;}.task-list li label { width: calc(100% - 100px); display: block; cursor: pointer;}.task-list li span {display: block;}.task-list li .btn { display: none; width: 80px; position: absolute; right: 10px; padding: 0.4em 1em; font-size: 13px; z-index: 10;}.task-list li:hover .btn { display: block;}
.inputForm { margin-bottom: 40px; background: #f9f3ee; padding: 40px 0; border-bottom: 1px solid #d7d2cd;}.inputForm .inner { display: flex;}.inputForm .input { width: 80%; font-size: 15px; outline: none; border: solid 3px #d7d2cd; padding: 10px; border-radius: 7px; margin-right: 10px;}.inputForm .input:focus { background: #f9f9f0;}.inputForm .btn { width: 20%;}.btn { font-size: 15px; cursor: pointer ; font-weight: bold; display: inline-block; padding: 10px 15px; text-decoration: none; background: #668ad8; color: #FFF; border-bottom: solid 4px #627295; border-radius: 7px; outline: none;}.btn:active { transform: translateY(4px); border-bottom: none; margin-bottom: 4px;}.btn.is-delete { background-color: #d86681; border-bottom-color: #956270;}
.checkbox-input { display: none;}.checkbox-label { padding-left: 30px; position: relative; margin-right: 20px;}.checkbox-label::before { content: ""; display: block; position: absolute; top: 2px; left: 0; width: 18px; height: 18px; border: 1px solid #d7d2cd; border-radius: 4px;}.checkbox-input:checked + .checkbox-label { color: #999;}.checkbox-input:checked + .checkbox-label::after { content: ""; display: block; position: absolute; top: 0; left: 5px; width: 7px; height: 14px; transform: rotate(40deg); border-bottom: 3px solid #d01137; border-right: 3px solid #d01137;}Types.tsx
TypeScriptは型を定義する必要があります。
最初にタスクの構造を考え作成しましょう。この定義はさまざまなコンポーネントから参照されるので、単独のファイルにしました。
export type Task = { id: number title: string done: boolean}TaskItem.tsx
一番下のコンポーネントにあたるTaskItem.tsxから作成します。
このコンポーネントはタスクのデータである、taskと、データを操作するイベントhandleDone、handleDeleteをPropsで受け取ります。
リストはulタグで作るのでこのコンポーネントはliで作ります。
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 type="checkbox" className="checkbox-input" onClick={() => handleDone(task)} defaultChecked={task.done} /> <span className="checkbox-label">{ task.title }</span> </label> <button onClick={() => handleDelete(task)} className="btn is-delete" >削除</button> </li> )}
export default TaskItemTaskList.tsx
TaskListコンポーネントは、タスクをチェックするhandleDone、削除するhandleDelete関数を作成して、先ほど作成したTaslItem.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 => t.id === task.id ? { ...task, done: !task.done } : t )) }
const handleDelete = (task: Task) => { setTasks(prev => prev.filter(t => t.id !== task.id )) }
return ( <div className="inner"> { tasks.length <= 0 ? '登録されたTODOはありません。' : <ul className="task-list"> { tasks.map( task => ( <TaskItem key={task.id} task={task} handleDelete={handleDelete} handleDone={handleDone} /> )) } </ul> } </div> )}
export default TaskListTaskInput.tsx
TaskInput.tsxは新しくタスクを登録する為の入力フォームです。
inputに入力して、追加ボタンを押すとsetTasksが実行され新規登録する動作をしています。
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> <div className="inputForm"> <div className="inner"> <input type="text" className="input" value={inputTitle} onChange={handleInputChange} /> <button onClick={handleSubmit} className="btn is-primary">追加</button> </div> </div> </div> )}
export default TaskInputApp.tsx
ルートとなるApp.tsxはあとで作成するTaskInputとTaskListを読み込んで配置します。
初期データであるinitialStateを作成して、タスク一覧(tasks)をuseStateの引数にいれます。
あとは先ほど作成したTaskInputとTaskListにPropsを渡すだけです。
const initialState: Task[] = [ { id: 2, title: '次にやるやつ', done: false },{ id: 1, title: 'はじめにやるやつ', done: true } ]
const App: React.FC = () => { const [tasks, setTasks] = useState(initialState)
return ( <div> <TaskInput setTasks={setTasks} tasks={tasks} /> <TaskList setTasks={setTasks} tasks={tasks} /> </div> )}小さいアプリでしたらこんな書き方でも問題ありませんが、ある程度の規模になると状態管理用の仕組みとかライブラリを使うと良いらしいですよ。ということで次回はReduxを使ってそのあたりをやっていきたいと思います。
