WEBOPIXEL

React.js + TypeScript でTodoアプリを作ってみる

Posted: 2020.01.15 / Category: javascript / Tag: ,

TypeScriptもReactもちょっとわかってきた! と思って最初に作るのはやっぱりTodoアプリですよね(?)。
ということで React.js + TypeScript でTodoアプリつくってみます。

Sponsored Link

環境構築

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と、データを操作するイベントhandleDonehandleDeleteをPropsで受け取ります。
リストはulタグで作るのでこのコンポーネントはliで作ります。

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はあとで作成するTaskInputTaskListを読み込んで配置します。
初期データであるinitialStateを作成して、タスク一覧(tasks)をuseStateの引数にいれます。
あとは先ほど作成したTaskInputTaskListに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を使ってそのあたりをやっていきたいと思います。

ここまでのソースコードはこちら

この記事の動画(Youtube)版

COMMENTS

tama 2020-01-15 22:13 

上記のだとstateを子コンポーネントで変えていますが親コンポーネントで処理を定義してあげてstateも親で定義してあげることってできますか?

webOpixel 2020-01-16 09:18 

親で関数を定義しても、Propsでそのまま渡せば子コンポーネントで実行することはできますよ。

LEAVE A REPLY

コードを書く場合は<pre>で囲んでください。