環境構築
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を使ってそのあたりをやっていきたいと思います。
ここまでのソースコードはこちら