終究都要學 React 何不現在學呢? - React 基礎 - TodoList (上) - (10)

前言

接下來這一章節我們將會實作一些小東西,主要是要整合前面的知識點,否則單看知識而沒有任何一點輸出的話是沒有任何用處的,因此就讓我們準備練習一下吧。

範本樣板

首先這邊我用 TailwindCSS 做了一個 ToDoList 範本,而這個範本主要會預先載入以下 CDN

因此會建議你往下看之前,可以先 Fork 回去到你自己的 CodePen 中,等一下你才方便實作,這裡面也包含了基本 HTML 樣板

ToDoList 範本

React ToDoList

接下來我們將會實作幾個功能

  1. 輸入代辦事項,點擊「」後出現在下方
  2. 完成的代辦事項可以打勾完成
  3. 可顯示幾筆代辦事項
  4. 清空全部任務(不論完成與否都清除)

初始化 React

接下來就讓我們開始實作,首先先將基本的 React 建立出來,然後這邊可以先將 #root 裡面的元素先全部丟進去 React return 中,接著你必須修正幾個錯誤,否則會無法正常運作

  • 第一個就是 JSX 本身是 JavaScript 擴充,因此 class 屬於 JavaScript 保留字,因此 class 要改成 className
  • 第二個也就是 Expected corresponding JSX closing tag for <input>,這個錯誤簡單來講就是 JSX 比較嚴謹,因此要調整 input 補上結尾標籤(也就是補上 /)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const App = () => {
return (
<div>
<div className="bg-indigo-500 p-5 h-screen">
<div className="max-w-[768px] m-auto bg-white p-5">
<h1 className="text-center text-2xl mb-4">React ToDoList</h1>
<div className="flex">
<input type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />
<button className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
</div>
<ul>
<li className="py-4">
<label>
<input type="checkbox" />
今天要洗碗
</label>
</li>
</ul>
<div className="flex justify-between items-center">
<p>
目前有 <span className="font-medium">1</span> 個事項待完成
</p>

<button type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Task</button>
</div>
</div>
</div>
</div>
)
}

const root = ReactDOM.createRoot(document.querySelector('#root'));
root.render(<App />);

如果你有依照以上步驟調整,你會發現 React 已經可以正常運作了,接著我們就可以開始實作功能了

宣告資料狀態

接下來要宣告一個初始資料狀態,是要拿來放 todo 新增的資料,這邊要注意的是初始資料會是一個陣列

1
const [ todoList, setTodoList ] = React.useState([]);

點擊事件綁定

那麼接下來我們要先替幾個東西綁定事件,也就是點擊事件,第一個是「+」按鈕,而這個事件我們先對應到一個函式叫做 addTodo

1
<button onClick={ addTodo } className="w-[50px] h-[50px] border-0 bg-sky-500 hover:bg-sky-600 rounded-r-lg text-white transition duration-700">+</button>
1
2
const addTodo = () => {
}

第二個則是 「Clear All Task」 按鈕,對應的是 remoteAllTodo

1
<button onClick={ remoteAllTodo } type="button" className="bg-red-300 p-2 rounded-md hover:bg-red-400 transition duration-700">Clear All Todo</button>
1
2
const remoteAllTodo = () => {
}

第三個就是勾選 todo 狀態的 checkbox,但是這邊我們先不綁事件,先把上面這兩個綁好就好,等一下後面再來一起處理。

取得 input 的值

第一個我們先來寫新增代辦的函式,當我們觸發 onClick 事件之後,要去取得 input 的 value 因此要稍微調整一下 input 給予一個 id

1
<input id="todoInput" type="text" className="w-full rounded-l-lg border-l-2 border-y-2 border-indigo-300 pl-4 focus:outline-indigo-500 focus:outline-none focus:outline-offset-0" placeholder="請輸入你的代辦事項" />

接著改寫一下 addTodo 函式

1
2
3
4
const addTodo = (event) => {
const value = document.querySelector('#todoInput').value;
console.log(value);
};

接下來你在 input 隨便輸入並按下「+」 就可以看到我們正確取得 input 的值了。

儲存 input 的值

當我們取得值之後,接下來就要將值推進去到 setTodoList 中,首先第一個會是解構原本資料的物件資料,第二個我們則要寫入另一個物件

1
2
3
4
5
6
7
8
9
const addTodo = () => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
name: input.value
}
])
};

之所以要先解構原本資料是因為我們不想要直接覆蓋掉原本的資料,而是要將新的資料推進去,這樣才不會造成資料的遺失,因此才會需要先解構原本的資料,接著再推進去新的資料。

接下來我們也要寫入代辦新增的時間,時間最簡單方式就是使用 Date.now(),剛好這個 Date.now() 非常適合作為一個 Key 使用,因此就會直接將它作為一個 Key 使用以及最後補個這個代辦事項的狀態(已完成與未完成)

1
2
3
4
5
6
7
8
9
10
11
const addTodo = (event) => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
id: Date.now(),
name: input.value,
status: false,
}
])
};

最後別忘記要清空 input 的欄位,畢竟新增成功就要清空

1
2
3
4
5
6
7
8
9
10
11
12
const addTodo = (event) => {
const input = document.querySelector('#todoInput');
setTodoList([
...todoList,
{
id: Date.now(),
name: input.value,
status: false,
}
])
input.value = '';
};

渲染代辦事項

接下來就是要渲染我們儲存 todoList 的代辦事項資料,如果是在 Vue 裡面我們會使用 v-for 來渲染畫面,而在 React 則是使用 map 並重新組合成一個 HTML 回傳給 JSX 來渲染資料

因此要將這一塊

1
2
3
4
5
6
7
8
<ul>
<li className="py-4">
<label>
<input type="checkbox" class="mr-2" />
今天要洗碗
</label>
</li>
</ul>

改成使用 map 組合 DOM,記得不要忘記 key 的存在,(記得補一個 checked)

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id }>
<label>
<input type="checkbox" class="mr-2" checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>

你可能會想說為什麼使用 map 就可以正常渲染資料?在前面章節我們有說過 JSX 本身會針對陣列展開 (Spread),而 map 會回傳一個陣列,因此就會正常渲染資料。

接著這邊你也可以順便把下方這一段改一下

1
2
3
<p>
目前有 <span className="font-medium">1</span> 個事項待完成
</p>

改成以下這樣就可以了

1
2
3
<p>
目前有 <span className="font-medium">{ todoList.length }</span> 個事項待完成
</p>

基本上到了這一步驟之後,你就可以開始試著新增一個代辦事項,然後你也可以在下方看到資料被渲染出來囉。

checkbox 事件綁定

那麼接下來我們要增加當使用者勾選任務後,會補上一個刪除線的效果,如同底下

這是一條刪除線

而判斷方式非常簡單,由於我們是使用 TailwindCSS 來實作,因此在 className 上面補上一個判斷,當 statetrue 的話,就會自動加上 line-through 樣式即可。

只是在開始之前我們要先針對 label 增加一個屬性,也就是 data-* 並傳入 key,這個我們稍後會使用到

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id }>
<label className={ todo.status ? 'line-through' : ''}>
<input type="checkbox" data-id={ todo.id } className="mr-2" checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>

這時候你新增每一筆資料就都可以看到 Li 多了一個 data-id 的屬性。

接下來也要針對 label 綁定一個事件,也就是 onClick 事件,對應到 updateTodo 方法

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id } data-id={ todo.id } >
<label className={ todo.status ? 'line-through' : ''} >
<input onClick={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>

updateTodo 會透過 event.target 去尋找 data-* 然後用這種方式去找出資料中符合 ID 的部分,並將該陣列資料轉換為 false or true

1
2
3
4
5
6
7
8
9
10
11
const updateTodo = (event) => {
const { id } = event.target.dataset;
const newTodoList = todoList.map((todo) => {
if(todo.id === Number(id)) {
todo.status = !todo.status;
}
return todo;
});

setTodoList([ ...newTodoList ]);
}

由於我們這邊解構出來的 dataset 都是一個字串,因此要記得補上 Number 做型別轉換。

接著這時候你可以嘗試輸入代辦事項並新增代辦,然後在打開 Console 應該會發現這個錯誤

Warning: You provided a checked prop to a form field without an onChange handler. This will render a read-only field. If the field should be mutable use defaultChecked. Otherwise, set either onChange or readOnly.

而這有幾種解決方式,將原本的 checked 改成 defaultChecked,或是在 input 上面增加一個 onChange 事件,這邊我們選擇後者。

1
2
3
4
5
6
7
8
9
10
11
12
<ul>
{
todoList.map((todo) => (
<li className="py-4" key={ todo.id } data-id={ todo.id } >
<label className={ todo.status ? 'line-through' : ''} >
<input onChange={ updateTodo } type="checkbox" className="mr-2" data-id={ todo.id } checked={ todo.status }/>
{ todo.name }
</label>
</li>
))
}
</ul>

這樣子就不會再出現該警告訊息了。

清空 Todo

恭喜你做到這邊的時候就已經將大部分功能給完成了!

所以最後就是單純的去撰寫 remoteAllTodo 裡面的功能,而這也其實非常簡單,因為我們只是要將代辦事項清空,因此只需要這樣寫即可

1
2
3
const remoteAllTodo = () => {
setTodoList([]);
};

非常簡單吧?你的第一個簡單版 React TodoList 就這樣完成了。

這邊也提供完整版 TodoList CodePen 的連結。

除此之外我也提供一版 Vue 版本的 TodoList CodePen 可以讓你比較一下兩者的差異。

後記

本文將會同步更新到我的部落格