TypeScriptで何か作ろうと思い立ち試しにReactとTypeScriptでTodoアプリを作ってみることにしました。
デモはこちら
※以下は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ファイルを作成して開発を進めることにします。
作成ファイルを以下4つです。
・Types.ts => Taskの型定義
・TaskItem.tsx => 単体Taskのコンポーネント
・TaskList.tsx => TaskItem.tsxの親コンポーネント
・TaskInput.tsx => 新規Taskを追加するためのコンポーネント
作成ではありませんがデフォルトでApp.tsxがあります。こちらも初回Stateやreturnをいじります。
全部で作業ファイルは5つになります。
今回は上記紹介ファイル順に開発を進めていくことにします。
3.todoアプリ開発
Types.ts で型定義
まずはTypes.tsから。
このコンポーネントはTaskの型を定義します。
TypeScriptはtype(型)定義を主として開発を進めていくので変数・引数などにtypeを指定します。
下記に型定義のパターンがまとめられていたので参考までに。
https://www.tohoho-web.com/ex/typescript.html
todoアプリなので、それぞれのタスク単体にどんな値が入るかtype定義します。
はじめにsrc
配下にcomponentsディレクトリを作成し、Types.tsを作成。
export type Task = {
id: number,
title: string,
done: boolean
}
これでタスクのidとtitleとタスク状態を定義しました。
それぞれnumber / string / booleanと定義しました。
exportとしているのは別のファイルで呼び出すからです。
もちろん別ファイルに直書きもできますが、同じ内容ならexportで使用した方がメンテナンス的にもよろしいですね。
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をマッピング
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で新規追加フォームの作成
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でコンポーネントを結合
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の型一覧
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
で構築完了