環境構築
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に入れます。
src/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は型を定義する必要があります。
最初にタスクの構造を考え作成しましょう。この定義はさまざまなコンポーネントから参照されるので、単独のファイルにしました。
src/components/Types.ts
export type Task = {
id: number
title: string
done: boolean
}
TaskItem.tsx
一番下のコンポーネントにあたるTaskItem.tsxから作成します。
このコンポーネントはタスクのデータである、taskと、データを操作するイベントhandleDone、handleDeleteをPropsで受け取ります。
リストはulタグで作るのでこのコンポーネントは
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
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 TaskItem
TaskList.tsx
TaskListコンポーネントは、タスクをチェックするhandleDone、削除するhandleDelete関数を作成して、先ほど作成したTaslItem.tsxに渡します。
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 =>
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 TaskList
TaskInput.tsx
TaskInput.tsxは新しくタスクを登録する為の入力フォームです。
inputに入力して、追加ボタンを押すとsetTasksが実行され新規登録する動作をしています。
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>
<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 TaskInput
App.tsx
ルートとなるApp.tsxはあとで作成するTaskInputとTaskListを読み込んで配置します。
初期データであるuseStateの引数にいれます。
あとは先ほど作成したTaskInputとTaskListにPropsを渡すだけです。
src/App.tsx
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を使ってそのあたりをやっていきたいと思います。
ここまでのソースコードはこちら
