この記事ではReactを使ったTODOアプリ制作の過程をアウトプット目的で記録していく。
なお、このアプリはUdemyの動画教材をもとに作成したものであり、オリジナルではありません。
利用したUdemyの動画はこちら。
制作するTODOアプリ概要
以下の画像のようなWebアプリを制作していく。
画面構成
・TODOを追加するエリア
・未完了のTODOエリア
・完了のTODOエリア
機能
・「追加」すると未完了のTODOに格納され、「完了」すると完了のTODOに移動する。
・「削除」すると未完了のTODOから削除され、「戻す」と未完了のTODOに移動する。
・TODO上限は5つまでとし、上限に達した場合はタスクを消化するよう警告が表示される。
ディレクトリ構成
初期段階のsrcフォルダには3つのファイルが格納されている。
・Todo.jsx
・index.jsx
・style.css
コーディング開始
流れ
1. Todo.jsxにマークアップを記述
2. style.cssにスタイルを記述
3. 未完了のTODOをstate
を意識したマークアップに改善
4. 完了のTODOをstate
を意識したマークアップに改善
5.タスクの追加機能の実装
6.タスクの削除機能の実装
7.タスクの完了機能の実装
8.タスクの戻す機能の実装
9.state
の初期値を削除する
10.コンポーネント化する
11.各コンポーネントに該当するcssのスタイルもコンポーネントで分ける
12.Todoの上限設定
1. Todo.jsxにマークアップを記述
マークアップはJSX記法に基づいて行なっていく。
最終的にはコンポーネントを使ってTODOの内容を表現するが、現段階では未完了のTODOと完了のTODOにそれぞれ仮のTODOを表示させておく。
import "./style.css";
export const Todo = () => {
return (
<>
<div>
<input placeholder="TODOを入力" />
<button>追加</button>
</div>
<div>
<p>未完了のTODO</p>
<ul>
<li>
<div>
<p>これからTODO</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
<li>
<div>
<p>これからTODO</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
</ul>
</div>
<div>
<p>完了のTODO</p>
<ul>
<li>
<div>
<p>終わったTODO</p>
<button>戻す</button>
</div>
</li>
<li>
<div>
<p>終わったTODO</p>
<button>戻す</button>
</div>
</li>
</ul>
</div>
</>
);
};
2. style.cssにスタイルを記述
最終的にスタイルはコンポーネントごとに記述を分けることになるが、現段階では全てstyle.cssに記述していく。
全ての要素に共通するスタイルは、このままstyle.cssに残ることになる。
body {
font-family: sans-serif;
color: #555;
font-weight: bold;
}
input {
border-radius: 8px;
border: none;
padding: 8px 16px;
margin-right: 5px;
}
button {
border-radius: 8px;
border: none;
padding: 4px 16px;
margin: 0px 2px;
}
button:hover {
background-color: #79a8a9;
color: #fff;
cursor: pointer;
}
.input-area {
background-color: #c6e5d9;
width: 400px;
height: 30px;
padding: 8px;
margin: 8px;
border-radius: 8px;
}
.incomplete-area {
border: solid 2px #79a8a9;
border-radius: 8px;
width: 400px;
min-height: 200px;
padding: 8px;
margin: 8px;
}
.complete-area {
border: solid 2px #79a8a9;
background-color: #c6e5d9;
border-radius: 8px;
width: 400px;
min-height: 200px;
padding: 8px;
margin: 8px;
}
.title {
text-align: center;
margin-top: 0;
font-weight: bold;
}
.list-row {
display: flex;
align-items: center;
}
.todo-item {
margin: 6px;
}
スタイルの適用に伴って、Todo.jsxにclass
を追加する。
import "./style.css";
export const Todo = () => {
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" />
<button>追加</button>
</div>
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
<li>
<div className="list-row">
<p className="todo-item">これからTODO</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
<li>
<div className="list-row">
<p className="todo-item">これからTODO</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
</ul>
</div>
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
<li>
<div className="list-row">
<p className="todo-item">終わったTODO</p>
<button>戻す</button>
</div>
</li>
<li>
<div className="list-row">
<p className="todo-item">終わったTODO</p>
<button>戻す</button>
</div>
</li>
</ul>
</div>
</>
);
};
3. 未完了のTODOをstate
を意識したマークアップに改善
未完了のTODOと完了のTODOのエリアは常に状態が変化していく要素となる。
状態なのでstate
で管理することになるため、それに合わせたマークアップにしていく。
未完了のTODO一覧をstate
で定義する
未完了のTODOは複数の要素となるので配列として定義する。
この時state
の定義にはuseState
を用いることを忘れずに。
useState
についてはこちら
今回useState
にはinCompleteTodos
とセット関数setIncompleteTodos
を配列に用意。
useState
の初期値には仮の配列としてこれからTODO1
とこれからTODO2
を設定。
useState
のAuto importも忘れずに。
import {useState} from "react";
import "./style.css";
export const Todo = () => {
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
return (
<>
//省略
</>
);
};
state
をもとにmap
メソッドを使ってli
タグ部分を表示する
具体的には、配列のstate
をもとに画面の要素を一覧で出していく部分をループしながらレンダリングしていく。
Reactでの一覧表示は、配列をmap
でループしながら一つずつ新しい要素を返却することで行う。
map
はメソッドなので関数(アロー関数)形式で記述し、引数にループする各要素が入る。
返却するのはli
タグの中身なので、li
タグ部分を丸々関数の中にコピペする。
p
タグの中身をmap
メソッドの引数に修正し、最終的なコードは以下のようになる。
import {useState} from "react";
import "./style.css";
export const Todo = () => {
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
return (
<>
//省略
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo) => {
return (
<li>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
);
})};
</ul>
</div>
//省略
</>
);
};
map
メソッドについてはこちら
key
を設定する
map
やfilter
において、配列の値をもとに一覧表示をする時には、key
の設定が必要となる。
key
設定は配列をループしてreturn
している一番上の要素に行い、今回はreturn
直下のli
タグが該当する。
例えば<li key={todo}>
のように設定する。
なぜkey
が必要なのか
Reactの仮想DOMは変更前と変更後の差分だけ抽出して、その差分のみ実際のDOMに反映していく。
そのため今回のようにループでレンダリングする場合、何個目の要素なのかを正確に比較するために目印をつけてあげる必要がある。
それでループしている各要素の一意になる項目をkey
に設定することとなる。
アロー関数の特性を活かして{}
とreturn
を省略する
今回return
の中身がひとまとまりとなっているので、アロー関数の省略記法を用いて記述を簡潔にしていく。
//省略
export const Todo = () => {
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
return (
<>
//省略
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo) => (
<li key={todo}> //keyの設定
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
))}
</ul>
</div>
//省略
</>
);
};
4. 完了のTODOをstate
を意識したマークアップに改善
未完了のTODOと同様の流れで完了のTODOをstate
で管理する記述に修正していく。
//省略
export const Todo = () => {
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
return (
<>
//省略
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo) => (
<li key={todo}> //keyの設定
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>完了</button>
<button>削除</button>
</div>
</li>
))}
</ul>
</div>
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
{completeTodos.map((todo) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>戻す</button>
</div>
</li>
))}
</ul>
</div>
</>
);
};
5.タスクの追加機能の実装
テキストボックスに入力した内容を、追加ボタンを押したら未完了のTODOに追加されるように機能を実装していく。
イメージは以下の通り
-
input
に入力した内容を取得する - それを
inCompleteTodos
の配列の中に追加する - すると
map
のループによって自動的に表示される
この時「入力された状態はなんなのか」という状態を管理するためstate
を用いる。
新しく以下のuseState
の記述を追加する。
state
名はtodoText
、更新関数はsetTodoText
とし、初期値には空文字を定義しておく。
export const Todo = () => {
const [todoText, setTodoText] = useState(""); //追加
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
return ( //省略
この入力された内容、すなわちtodoText
という値はinput
におけるvale
に当たるので、value
属性にtodoText
を設定しておく。
export const Todo = () => {
const [todoText, setTodoText] = useState(""); //追加
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} /> //属性を追加
<button>追加</button>
</div>
//省略
例えばこの状態でuseState
の初期値にtest
と入力すると、画面のinput
部分には「test」 と表示されるようになる。
しかし、表示画面からテキストボックスに直接入力しようとしても入力できない状態になっている。
これは、初期値が空文字に設定していて、その値を常にvalueに設定しているので常に空文字状態になってしまっていることが原因。
解決方法として、テキストボックスにユーザーが変更を加えたことを検知して、それをもとに常にtodoText
のstate
を更新するという処理を実装する。
input
の入力内容の変更を検知させる
input
の入力内容を自動で検知するにはonChange
イベントを用いる。
onChange
はテキストボックスに変更があったときに実行されるイベント。
-
input
タグ内にイベントを追加
<input placeholder="TODOを入力" value={todoText} onChange={} />
-
onChange
が実行された時の関数を定義
onChangeTodoText
という名前で定義するので、その関数をonChange
イベントで呼び出す。
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
- 関数の中身を入力された内容を検知するように記述
このようなイベントは自動でevent
という引数が渡ってくる。
そして、setTodoText
のevent
のtarget
のvalue
というところに入力された文字が入ってくるように関数を書く。
export const Todo = () => {
//省略
const onChageTodoText = (event) => setTodoText(event.target.value);
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button>追加</button>
</div>
//省略
結果としてinput
に値が入力されるとonChange
イベントが発火し、その内容をもとにtodoText
のstate
を更新して、そのtodoText
の値がvalue
に常に設定されるという流れを作ることができた。
input
に入力された値とtodoText
のstate
の値が常にイコール状態を保てる状態になったので、入力した値が`value'となって画面に表示される。
追加ボタンを押した時の処理を作る
-
button
タグにクリックイベントを割り当てる。 - そしてイベントが発火した時の関数として
onClickAdd
という関数を定義する。
export const Todo = () => {
const [todoText, setTodoText] = useState(""); //追加
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
};
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
//省略
- テキストボックスに入力された内容を
inCompleteTodo
の配列に追加する
配列に追加する、つまり配列の結合なのでスプレッド構文を使用する。
まず追加した新しい配列を格納する関数をnewTodos
として用意する。
そして新しい配列を生成したいので、スプレッド構文を使って現在のinCompleteTodos
を配列で設定。
(inCompleteTodos
の配列のコピーをnewTodos
として生成)
export const Todo = () => {
const [todoText, setTodoText] = useState(""); //追加
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
const newTodos = [...inCompleteTodos];
};
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
//省略
スプレッド構文についてはこちら
newTodos
にコピーした配列に追加したい要素はtodoText
の値なので、newTodos
の配列に追記する。
const newTodos = [...inCompleteTodos, todoText];
そして、新しい要素が追加された配列newTodos
を更新される値、つまりsetInCompleteTodos
に渡せば現在の状態に新たな配列が追加された状態になる。
export const Todo = () => {
const [todoText, setTodoText] = useState(""); //追加
const [inCompleteTodos, setInCompleteTodos] = useState(["これからTODO1", "これからTODO2"]);
const [completeTodos, setCompleteTodos] = useState(["終わったTODO1", "終わったTODO2"]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
};
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
//省略
この段階で、テキストボックスに入力した内容を追加ボタンをクリックすることで未完了のTODOに追加することができるようになった。
追加した時にテキストボックスの入力を空にする
これは簡単で、setInCompleteTodoText
にnewTodos
を渡した後に、setTodoText
に空文字を渡すだけで処理が完了する。
export const Todo = () => {
//省略
const onClickAdd = () => {
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText(""); //空文字を設定
};
//省略
また、現在の状態では何も入力してない時に追加ボタンをクリックすると、空のTODOが追加されてしまうので、その問題を条件式で解消する。
todoText
が空の時はreturn
させる
テキストボックスが状態では追加できないように条件式を記述する。
これで空文字の時はif文の行でreturn
されるためそこで処理が止まり、その下の配列の追加処理は行われなくなるため、空文字のTODOは追加できなくなった。
export const Todo = () => {
//省略
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText(""); //空文字を設定
};
//省略
6.タスクの削除機能の実装
削除ボタンにクリックイベントを付与し、削除ボタンが押されたときに、押された対象の行のcompleteTodos
の要素を削除するという流れの機能になる。
イベントを用意し関数を定義する
イベントで呼び出す関数名はonCkickDelete
とする。
export const Todo = () => {
//省略
const onClickDelete = () => {}
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>完了</button>
<button onClick={onClickDelete}>削除</button>
</div>
</li>
))}
</ul>
</div>
削除されたのが配列の何番目かを取得する
現在map
の部分には何番目かを判定する要素がないので、それを追加していく。
map
の第二引数にはindex
が入ってくるのでこれを活用する。
index
は0番目のループ、1番目のループというようにインデックス番号を表す引数。
map
の第二引数にindex
を渡し、onClickDelete
関数の引数にもindex
を渡すことで、onClickDelete
はindex
を引数として受け取る関数になる。
そして試しにalert
でindex
の値が表示されるようにし、イベントの引数にもindex
を持たせることでアラートを実装する。
export const Todo = () => {
//省略
const onClickDelete = (index) => {
alet(index);
}
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button>完了</button>
<button onClick={onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
すると常にアラートが表示されるという、アラートが暴走するバグが発生してしまう。
原因は、イベント部分を{}
で記述しているためJSとして解釈されてしまい、ループでその部分を通るたびに関数が実行されてしまっているから。
それでJSX記法をしている部分から引数を設定した関数を実行したときは、{}
の中に関数を生成してあげる。
<button onClick={() => onClickDelete(index)}>削除</button>
そうすることで{}
の中身は関数としてではなく、関数を実行した時の処理として認識されるようになる。
すなわち、イベントが発火した時にonClickDelete
が即時実行されることがなくなったので、アラートの暴走が止まる。
この状態で未完了のTODOにあるリストの削除ボタンをクリックすると、アラートでindex
の値が正常に表示されるようになる。
取得したindex
をもとにinCompleteTodos
からn番目の値を削除する処理を実装
削除した後の新しい配列を取得したいので、追加の時と同様にnewTodos
という関数を用意。
そしてスプレッド構文を用いて'newTodos'に今の未完了リストの一覧、すなわちinCompleteTodos
をコピーした配列を用意する。
export const Todo = () => {
//省略
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
}
return (
//省略
- コピーした配列に対して
index
番目の要素を削除する機能を実装
JSのsplice
メソッドを使う。
splice
は第一引数にindex
、第二引数に1
を指定することで、何番目のindex
の要素から1個削除するという処理になる。
export const Todo = () => {
//省略
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
}
return (
//省略
上記の記述だと、onClickDelete
の引数index
が0番目だとしたら、0番目の要素から1個削除するという機能になっている。
つまり0番目の要素だけ削除することがでできる。
最後にsetInCompleteTodos
にnewTodos
を渡せばいよい。
export const Todo = () => {
//省略
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
return (
//省略
これで、新しいTODOの追加と削除ボタンの機能の実装が完了した。
7.タスクの完了機能の実装
未完了のTODOの完了ボタンをクリックすると、完了のTODOに移動する処理を実装していく。
完了ボタンも削除ボタンと同様に「何番目の要素を」という考え方を使うので、削除ボタンのクリックイベントと同じ方で実装することができる。
関数名はonClickComplete
とし引数にindex
を持たせる。
そして削除ボタンと同様に関数を定義し、引数にindex
を持たせておく。
- 完了を押したら未完了TODOから削除する
- 完了を押したら完了TODOの一番下に追加する
という処理が必要になるので、順に行っていく。
未完了TODOから削除する
削除の部分はonClickDelete
関数の上2行をコピペする。
ただし変数の名前だけ、今回は完了したTODOも対象になってくるのでnewTodos
ではなくnewInCompleteTodos
としておく。
export const Todo = () => {
//省略
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
}
return (
//省略
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
後はstate
を更新すれば完了ボタンを押した時に未完了TODOから削除されるようになる。
その前に完了TODOに追加する機能を実装する。
完了TODOに追加する
配列のstate
を更新していくので新しくnewCompleteTodos
という新しい完了TODOの一覧のstate
を用意する。
これも既存の完了TODOの配列(completeTodos
)に、完了ボタンが押された行の要素を追加することになるので、スプレッド構文でこれまでと同じように実装していく。
完了ボタンが押された行の要素の取得はinCompleteTodos
のindex
を取得すれば良いので、以下のような記述になっていく。
export const Todo = () => {
//省略
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
}
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]]; //ここを追加
return (
//省略
これで新しい完了TODOの一覧もできたので、未完了TODO(setInCompleteTodos
)と完了TODO(setCompleteTodos
)のそれぞれのstate
を更新していく。
export const Todo = () => {
//省略
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
return (
//省略
これで、タスクの追加・削除・完了の機能の実装ができた。
8.タスクの戻す機能の実装
完了ボタンの逆の流れを行う。
まずはmap
の第二引数にindex
を渡し、イベントの記述、onClickBack
という関数を用意する。
export const Todo = () => {
//省略
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
}
return (
//省略
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
{completeTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClicK={() => onClickBack(index)}>戻す</button>
</div>
</li>
))}
</ul>
</div>
</>
処理としては以下の通りなので順に行っていく
- 戻すボタンを押すと完了TODOから削除される
- 戻すボタンを押すと未完了TODOの一番下に追加される
戻すボタンを押すと完了TODOから削除される
export const Todo = () => {
//省略
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
}
return (
//省略
戻すボタンを押すと未完了TODOの一番下に追加される
追加の処理を記述し、最後にそれぞれのstate
を更新する。
export const Todo = () => {
//省略
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
//省略
9.state
の初期値を削除する
TODOアプリに必要なすべての機能の実装ができたので、初期値として入れていた値を削除する。
最終的なコードは以下の通り。
import {useState} from "react";
import "./style.css";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [inCompleteTodos, setInCompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
<>
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
{completeTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClicK={() => onClickBack(index)}>戻す</button>
</div>
</li>
))}
</ul>
</div>
</>
);
};
10.コンポーネント化する
現在「入力エリア」「未完了エリア」「完了エリア」が同じファイルに記述されているので、これをコンポーネントで分割して可読性を向上する。
- 入力エリア
ディレクトリを追加
srcフォルダ配下に新しくcomponents
というフォルダを作り、さらにその配下に「入力エリア」用のInputTodo.jsx
というファイルを作成。
関数コンポーネントを記述
export
と関数コンポーネントInputoTodo
を用意したら、Todo.jsxから該当部分を切り取って関数の中にペーストする。
export const InputTodo = () => {
return (
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
)
}
この状態だとtodoText
やonChageText
などの変数や関数が同ファイル内に存在しないのでエラーになっている。
そのためprops
で受け取ったものを参照できるようにリファクタリングしていく。
props
を受け取るようリファクタリング
props
を受け取る側であるInputTodo.jsxの関数コンポーネントの引数にprops
を渡す。
export const InputTodo = (props) => {
return (
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChangeTodoText} />
<button onClick={onClickAdd}>追加</button>
</div>
)
}
そして呼び出し側のTodo.jsxでは切り取った部分にコンポーネントを呼び出し、Auto importでインポートしておく。
さらに関数もファイル内に存在していないためエラーが吐かれている。
この辺りもprops
で渡していく。
具体的には以下の3つをそれぞれTodo.jsx側でprops
で渡していく。
todoText
onChangeTodoText
onClickAdd
まずInputTodo
コンポーネントの部分で、todoText
というprops
名でtodoText
を渡す。
同様にonChangeTodoText
は省略してonChange
、onClickAdd
は省略してonClick
というprops
名で、それぞれ呼び出す関数を渡していく。
import {useState} from "react";
import {InputTodo} from "./components/InpotTodo";
import "./style.css";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [inCompleteTodos, setInCompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
<>
<InputTodo todoText={todoText} onChange={onChageTodoText} onClick={onClickAdd} />
//省略
</>
);
};
これで呼び出し側のコードが完成したので、次はコンポーネント側で渡されたprops
を使えるようにリファクタリングしていく。
props
を使えるようにリファクタリング
オブジェクトの分割代入で、todoText
,onChange
,onClick
の3つのprops
を取り出す。
そして名前を変えてprops
を使った部分に合わせて、InputTodo.jsxの変数や関数名を修正する。
export const InputTodo = (props) => {
const {todoText, onChange, onClick} = props;
return (
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChange} /> //元はonChangeTodoText
<button onClick={onClick}>追加</button> //元はonClickAdd
</div>
)
}
これでコンポーネントが完成。
- 未完了エリア
ディレクトリを追加
InCompleteTodos.jsx
というファイルを作成。
関数コンポーネントを記述
export
と関数コンポーネントInCompleteTodos
を用意したら、Todo.jsxから該当部分を切り取って関数の中にペーストする。
export const InCompleteTodos = () => {
return (
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
)
}
この状態だとinCompleteTodos
やonClickComplete
などの変数や関数が同ファイル内に存在しないのでエラーになっている。
そのためprops
で受け取ったものを参照できるようにリファクタリングしていく。
props
を受け取るようリファクタリング
props
を受け取る側であるInCompleteTodos.jsxの関数コンポーネントの引数にprops
を渡す。
export const InCompleteTodos = (props) => {
return (
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{inCompleteTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
)
}
そして呼び出し側のTodo.jsxでは切り取った部分にコンポーネントを呼び出し、Auto importでインポートしておく。
さらに関数もファイル内に存在していないためエラーが吐かれている。
この辺りもprops
で渡していく。
具体的には以下の3つをそれぞれTodo.jsx側でprops
で渡していく。
inCompleteTodos
onClickComplete
onClickDelete
import {useState} from "react";
import {InputTodo} from "./components/InpotTodo";
import {InCompleteTodos} from "./components/InCompleteTodos";
import "./style.css";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [inCompleteTodos, setInCompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
<>
<InputTodo todoText={todoText} onChange={onChageTodoText} onClick={onClickAdd} />
<InCompleteTodos todos={inCompleteTodos} onCkickComplete={onCkickComplete} onCkickDelete={onCkickDelete} />
//省略
</>
);
};
これで呼び出し側のコードが完成したので、次はコンポーネント側で渡されたprops
を使えるようにリファクタリングしていく。
props
を使えるようにリファクタリング
オブジェクトの分割代入で、todos
,onCkickComplet
,onCkickDelete
の3つのprops
を取り出す。
そして名前を変えてprops
を使った部分に合わせて、InCompleteTodos.jsxの変数や関数名を修正する。
export const InputTodo = (props) => {
const {todos, onCkickComplet, onCkickDelete} = props;
return (
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{todos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
)
}
これでコンポーネントが完成。
- 完了エリア
ディレクトリを追加
CompleteTodos.jsx
というファイルを作成。
関数コンポーネントを記述
export
と関数コンポーネントCompleteTodos
を用意したら、Todo.jsxから該当部分を切り取って関数の中にペーストする。
export const CompleteTodos = () => {
return (
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
{completeTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClicK={() => onClickBack(index)}>戻す</button>
</div>
</li>
))}
</ul>
</div>
)
}
この状態だとCompleteTodos
やonClickBack
の変数や関数が同ファイル内に存在しないのでエラーになっている。
そのためprops
で受け取ったものを参照できるようにリファクタリングしていく。
props
を受け取るようリファクタリング
props
を受け取る側であるInCompleteTodos.jsxの関数コンポーネントの引数にprops
を渡す。
export const CompleteTodos = (props) => {
return (
<div className="complete-area">
<p className="title">完了のTODO</p>
<ul>
{completeTodos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClicK={() => onClickBack(index)}>戻す</button>
</div>
</li>
))}
</ul>
</div>
)
}
そして呼び出し側のTodo.jsxでは切り取った部分にコンポーネントを呼び出し、Auto importでインポートしておく。
さらに関数もファイル内に存在していないためエラーが吐かれている。
この辺りもprops
で渡していく。
具体的には以下の3つをそれぞれTodo.jsx側でprops
で渡していく。
CompleteTodos
onClickBack
import {useState} from "react";
import {InputTodo} from "./components/InpotTodo";
import {InCompleteTodos} from "./components/InCompleteTodos";
import {CompleteTOdos} form "./components/CompleteTodos";
import "./style.css";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [inCompleteTodos, setInCompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
<>
<InputTodo todoText={todoText} onChange={onChageTodoText} onClick={onClickAdd} />
<InCompleteTodos todos={inCompleteTodos} onCkickComplete={onCkickComplete} onCkickDelete={onCkickDelete} />
<CompleteTodos todos={CompleteTodos} onClickBack={onClickBack} />
</>
);
};
これで呼び出し側のコードが完成したので、次はコンポーネント側で渡されたprops
を使えるようにリファクタリングしていく。
props
を使えるようにリファクタリング
オブジェクトの分割代入で、todos
,onBack
の2つのprops
を取り出す。
そして名前を変えてprops
を使った部分に合わせて、CompleteTodos.jsxの変数や関数名を修正する。
export const InputTodo = (props) => {
const {todos, onCkickBack = props;
return (
<div className="incomplete-area">
<p className="title">未完了のTODO</p>
<ul>
{todos.map((todo, index) => (
<li key={todo}>
<div className="list-row">
<p className="todo-item">{todo}</p>
<button onClick={() => onClickComplete(index)}>完了</button>
<button onClick={() => onClickDelete(index)}>削除</button>
</div>
</li>
))}
</ul>
</div>
)
}
これでコンポーネントが完成。
####最終的なTodo.jsx
親コンポーネントのApp
のreturn
の中身が3つのコンポーネントのエリアに分かれていて、各コンポーネントの役割が名前からわかる用の異なった。
さらにコンポーネントしておくことによって、使いまわしたいときに1行のコピペだけで済むようになった。
import {useState} from "react";
import {InputTodo} from "./components/InpotTodo";
import {InCompleteTodos} from "./components/InCompleteTodos";
import {CompleteTOdos} form "./components/CompleteTodos";
import "./style.css";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [inCompleteTodos, setInCompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChageTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...inCompleteTodos, todoText];
setInCompleteTodoText(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...inCompleteTodos];
newTodos.splice(index, 1)
setInCompleteTodos(newTodos);
}
const onClickComplete = (index) => {
const newInCompleteTodos = [...inCompleteTodos];
newInCompleteTodos.splice(index, 1)
const newCompleteTodos = [...completeTodos, inCompleteTodos[index]];
setInCompleteTodos(newInCompleteTodos);
setCompleteTodos(newCompleteTodos);
}
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newInCompleteTodos = [...inCompleteTodos, completeTodos(index)];
setCompleteTodos(newCompleteTodos);
setInCompleteTodos(newInCompleteTodos);
}
return (
<>
<InputTodo todoText={todoText} onChange={onChageTodoText} onClick={onClickAdd} />
<InCompleteTodos todos={inCompleteTodos} onCkickComplete={onCkickComplete} onCkickDelete={onCkickDelete} />
<CompleteTodos todos={CompleteTodos} onClickBack={onClickBack} />
</>
);
};
11.コンポーネントに該当するcssのスタイルもコンポーネントで分ける
今回はReactが用意しているインラインスタイルでコンポーネント化する。
またInputoTodo
コンポーネント以外のコンポーネントはclass
を使いまわしている部分があるので、今回はInputoTodo
コンポーネントのスタイルのみ格納する。
各コンポーネントのclassName
は削除し、インラインスタイルでスタイルを記述していく。
可読性を考慮し一度変数に挟む形でコンポーネントに格納していく。
判定式はisMaximiIncompleteTodos
という変数にすべて格納することで、仕様変更があった場合でも簡単に変更できるようにしておく
const style = {
backgroundColor: "#c6e5d9",
width: "400px",
height: "30px",
padding: "8px",
margin: "8px",
borderRadius: "8px",
}
export const InputTodo = (props) => {
const {todoText, onChange, onClick} = props;
return (
<div className="input-area">
<input placeholder="TODOを入力" value={todoText} onChange={onChange} /> //元はonChangeTodoText
<button onClick={onClick}>追加</button> //元はonClickAdd
</div>
)
}
12.Todoの上限設定
InputoTodo
とInCompleteTodos
の間にテキストを表示する
タグを用意。
そしてincompleteTodos
の配列の長さが5個以上の時だけ表示させたいので、{}
の中に論理演算子で判定処理を記述。
import { useState } from "react";
import { InputTodo } from "./components/InputTodo";
import { IncompleteTodos } from "./components/IncompleteTodos";
import "./styles.css";
import { CompleteTodos } from "./components/CompleteTodos";
export const Todo = () => {
const [todoText, setTodoText] = useState("");
const [incompleteTodos, setIncompleteTodos] = useState([]);
const [completeTodos, setCompleteTodos] = useState([]);
const onChangeTodoText = (event) => setTodoText(event.target.value);
const onClickAdd = () => {
if (todoText === "") return;
const newTodos = [...incompleteTodos, todoText];
setIncompleteTodos(newTodos);
setTodoText("");
};
const onClickDelete = (index) => {
const newTodos = [...incompleteTodos];
newTodos.splice(index, 1);
setIncompleteTodos(newTodos);
};
const onClickComplete = (index) => {
const targetTodo = incompleteTodos[index]; // spliceする前に取得
const newIncompleteTodos = [...incompleteTodos];
newIncompleteTodos.splice(index, 1);
const newCompleteTodos = [...completeTodos, targetTodo]; // ここでtargetTodoを使う
setIncompleteTodos(newIncompleteTodos);
setCompleteTodos(newCompleteTodos);
};
const onClickBack = (index) => {
const newCompleteTodos = [...completeTodos];
newCompleteTodos.splice(index, 1);
const newIncompleteTodos = [...incompleteTodos, completeTodos[index]];
setCompleteTodos(newCompleteTodos);
setIncompleteTodos(newIncompleteTodos);
};
const isMaximiIncompleteTodos = incompleteTodos.length >= 5;
return (
<>
<InputTodo
todoText={todoText}
onChange={onChangeTodoText}
onClick={onClickAdd}
disabled={isMaximiIncompleteTodos}
/>
{isMaximiIncompleteTodos && (
<p style={{ color: "red" }}>
上限に達しました。タスクを消化してください。
</p>
)}
<IncompleteTodos
todos={incompleteTodos}
onClickComplete={onClickComplete}
onClickDelete={onClickDelete}
/>
<CompleteTodos todos={completeTodos} onClickBack={onClickBack} />
</>
);
};
そしてInputTodo
コンポーネントのinput
とbutton
にdisabled
という記述を追加することで、条件に達したときに機能できないようにする。
const style = {
backgroundColor: "#c6e5d9",
width: "400px",
height: "30px",
padding: "8px",
margin: "8px",
borderRadius: "8px",
};
export const InputTodo = (props) => {
const { todoText, onChange, onClick, disabled } = props;
return (
<div style={style}>
<input
disabled={disabled}
placeholder="TODOを入力"
value={todoText}
onChange={onChange}
/>
<button disabled={disabled} onClick={onClick}>
追加
</button>
</div>
);
};
以上でReactを使ったTODOアプリの制作が完了。