owned this note
owned this note
Published
Linked with GitHub
# 【範例】把 Todo List 改為 React
###### tags: `javascript`、`react`
這篇文章要來翻修之前所寫的 Todo List 程式碼,把它們都改成使用 React 來寫,而這個 Todo List 原本的功能有新增、刪除、標註已完成或未完成,因此待會 React 也照樣實作出這些功能。
首先先放上原先的 Todo List 程式碼。
`index.html` 檔案:
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Week21 Todo List</title>
<link rel="stylesheet" href="./style.css">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
</head>
<body>
<nav id="Home" class="navbar navbar-light bg-light">
<a class="navbar-brand" href="#Home">Todo List</a>
</nav>
<main>
<div class='create-item'>
<input type="text" name="input-item" id="input-item" placeholder="請輸入項目名稱">
<button type="button" class="add-btn btn btn-primary btn-sm">Add</button>
</div>
<div class="Todo-items">
<ul>
</ul>
</div>
</main>
<script type="text/javascript" src="./main.js"></script>
</body>
</html>
```
`style.css` 檔案:
```css=
body {
text-decoration: none;
}
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
position: relative;
bottom: -5px;
width: 300px;
}
ul {
padding: 5px;
}
li {
list-style-type: none;
margin-bottom: 10px;
font-size: 20px;
position: relative;
}
main {
max-width: 600px;
margin: 0 auto;
}
.create-item {
margin-top: 10px;
}
#input-item {
width: 300px;
}
.add-btn {
margin: 10px;
}
.delete-btn {
border-radius: 6px;
background-color: #ff7f76;
color: black;
font-size: 14px;
padding: 4px 8px;
}
.checked {
color: #ccc;
text-decoration: line-through;
font-style: italic;
}
input[type=checkbox] {
margin-right: 10px;
transform: scale(1.5);
}
```
`main.js` 檔案:
```javascript=
// initialize variable
let itemsList = [];
let id = 1;
// get all todo items data from locoalStorage
function getData() {
const todoData = window.localStorage.getItem('todoapp');
if (todoData) {
itemsList = JSON.parse(todoData);
}
}
// get the latest id from todo list and then update id value
function setId() {
if (itemsList[itemsList.length - 1] !== undefined) {
id = itemsList[itemsList.length - 1].id + 1;
}
}
// set todo items data to localStorage
function setData() {
window.localStorage.setItem('todoapp', JSON.stringify(itemsList))
}
function render() {
$('ul').empty();
for (let i = 0; i < itemsList.length; i += 1) {
$('ul').append(`
<li class='item' data-id=${itemsList[i].id}>
<input type='checkbox' ${itemsList[i].checked ? 'checked' : ''} />
<span class='item-name ${itemsList[i].checked ? 'checked' : ''}'>${itemsList[i].todo}</span>
<button class='delete-btn'>X</button>
</li>
`);
}
}
$(document).ready(() => {
$('.add-btn').on('click', () => {
const inputValue = $('#input-item').val();
if (inputValue !== '') {
itemsList.push({ todo: inputValue, checked: false, id });
render();
}
$('#input-item').val('');
id += 1;
setData();
});
$('.Todo-items').on('click', '.delete-btn', (event) => {
const deletedItem = $(event.target).parent().attr('data-id');
itemsList = itemsList.filter(item => item.id !== Number(deletedItem));
render();
setData();
});
$('.Todo-items').on('click', 'input[type=checkbox]', (event) => {
const id = Number($(event.target).parent().attr('data-id'));
for (let i = 0; i < itemsList.length; i += 1) {
if (itemsList[i].id === id && itemsList[i].checked === false) {
itemsList[i].checked = true;
} else if (itemsList[i].id === id && itemsList[i].checked === true) {
itemsList[i].checked = false;
}
}
render();
setData();
})
});
getData();
setId();
render();
```
待會改為 React 的時候,先不處理 `localStorage` 的部份。
UI 畫面長這樣:
![](https://i.imgur.com/fI6Q4Li.png)
## 改為 React
首先先準備 `app.js`、`index.js`、`webpack.config.js` 三個檔案。
`app.js` 檔案主要是使用 JSX 語法來寫 UI 畫面的檔案,其內容為:
```jsx=
import React, { Component } from 'react'
import './style.css' // 引入 style.css
class App extends Component {
render() {
// render index.html 的 DOM 結構
return (
<div>
<nav id="Home" className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#Home">Todo List</a>
</nav>
<main>
<div className='create-item'>
<input type="text" name="input-item" id="input-item" placeholder="請輸入項目名稱" />
<button type="button" className="add-btn btn btn-primary btn-sm">Add</button>
</div>
<div className="Todo-items">
<ul>
</ul>
</div>
</main>
</div>
)
}
}
export default App
```
先把原先的 Todo List 使用的 `style.css` 引入到 `app.js` 裡面來,然後把 `index.html` 檔案的 `<body>` 內容都寫到 `render` 當中。
而一開始的 `index.html` 可以改為這樣:
```htmlmixed=
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Week21 Todo List</title>
// 假如 app.js 不引入 css 檔案,那麼原先的那段 link css 檔案可以留著但是路徑要注意一下
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css"
integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"
integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js"
integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1"
crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"
integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM"
crossorigin="anonymous"></script>
</head>
<body>
<div id='root'></div>
</body>
</html>
```
在 `<body>` 只留下 `id='root'` 的 DOM 節點。
`index.js` 主要是用來當作中繼器,把 `app.js` 和 `index.html` 給連接起來,而這個過程是要藉由 webpack 來實施,也因此 `index.js` 也是 `webpack.config.js` 的 entry 檔案。而`index.js` 的內容為:
```jsx=
import React from 'react'
import ReactDOM from 'react-dom'
import App from './app.js' // 從 app.js 檔案引入 App 這個 Component
// 當執行的時候,會把 App Component render 到 root 這個 DOM 上面去
ReactDOM.render(<App />, document.getElementById('root'))
```
下面則為 `webpack.config.js` 的程式碼:
```javascript=
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index.js", // 引入 index.js
output: {
path: path.join(__dirname, "/dist"),
filename: "bundle.[hash].js"
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.css$/,
use: [
{ loader: 'style-loader' },
{ loader: 'css-loader' }
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: './index.html'
})
]
}
```
使用了 `css-loader`、`style-loader` 和 `html-webpack-plugin`。
`css-loader`、`style-loader` 這兩個要安裝才能在 `app.js` 引入 CSS 檔案,然後在 `use` 裡面放的位置不能顛倒,`css-loader` 一定要在最下面或是最右邊。
而 `html-webpack-plugin` 是一個很方便的 plugin,會自動根據 JavaScript 檔名變化(輸出的 `bundle.js` 檔案使用`[hash]`)而產生新的相對應的 `index.html` 檔案,所以說,上面的 `index.html` 才沒有使用 `<script>` 引入 JavaScript 檔案。
接著 `npm run start`(`webpack --node development`)就可以產生我們要的 `index.html` 檔案了,或是使用實務上通常會用的 `webpack-dev-server` 這個 module 來開發,會比較方便。
初始畫面長這樣:
![](https://i.imgur.com/XxB88wL.png)
### 每次的資料 render
我們先處理在 `app.js` 當中的 `render` 內容,因為每次 render 都會用到這邊,下面我們要做的目標是把在一份 todo list 表單所有 todo 項目的資料一一地 render 出來。
而在 React 如果你要把項目照順序 render 出來最常用的方法是 `map`,像這樣做:
```jsx=
const itemsList = [1, 2, 3]
<div className="Todo-items">
{itemsList.map(todo => (
<li>{todo}</li>
))}
</div>
```
`itemsList` 就是放所有 todo 項目資料的表單,我們可以使用 JavaScript 的 `map` 把每個項目照順序一一 render 出來,可以把上面那段看做是這麼運作的:
```jsx=
<div className="Todo-items">
{[<li>{1}</li>,<li>{2}</li>,<li>{3}</li>]}
</div>
```
也就是在 React 中,一串陣列的資料如果放到 `render` 去運作的話,它就會幫你從陣列的第一個項目 render ,render 到陣列最後一個項目。
我們先把在 `main.js` 的 `render` 這個 function 在每次要 render 時會產生的內容:
```htmlmixed=
<li class='item' data-id=${itemsList[i].id}>
<input type='checkbox' ${itemsList[i].checked ? 'checked' : ''} />
<span class='item-name ${itemsList[i].checked ? 'checked' : ''}'>${itemsList[i].todo}</span>
<button class='delete-btn'>X</button>
</li>
```
上面那段內容貼到 `App` Component 的 `render` 當中,並且放入 `map` 做處理:
```jsx=
class App extends Component {
constructor(props) {
super(props)
this.state = {
itemsList: []
}
this.id = 1
}
render() {
const { itemsList } = this.state
return (
<div>
<nav id="Home" className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#Home">Todo List</a>
</nav>
<main>
<div className='create-item'>
<input type="text" name="input-item" id="input-item" placeholder="請輸入項目名稱" />
<button type="button" className="add-btn btn btn-primary btn-sm">Add</button>
</div>
<div className="Todo-items">
<ul>
{itemsList.map(todo => (
<li className='item'>
// {todo.checked ? 'checked' : ''} 有問題,下面的範例先省略
<input type='checkbox' {todo.checked ? 'checked' : ''} />
<span className={`item-name ${todo.checked ? 'checked' : ''}`}>{todo.todo}</span>
<button className='delete-btn'>X</button>
</li>
))}
</ul>
</div>
</main>
</div >
)
}
}
```
上面程式碼建立了 `App` 的 `constructor`,並且設定兩個 `state` 分別是 `itemsList` 和 `id`。接著就對從 `main.js` 貼過來的內容做 JSX 語法的轉換,同時刪除在 `<li>` 標籤的 `data-id` 這個屬性,後面會另外做解釋為什麼刪除。
### 新增的功能
再來我們來做新增項目的功能:
```jsx=
class App extends Component {
constructor(props) {
super(props)
this.state = {
itemsList: [],
todoText: ''
}
this.id = 1
this.handleChange = this.handleChange.bind(this)
this.addTodo = this.addTodo.bind(this)
}
handleChange(e) {
this.setState({
todoText: e.target.value
})
}
addTodo() {
const { todoText, itemsList } = this.state
this.setState({
itemsList: [...itemsList, {
id: this.id,
checked: false,
todo: todoText
}],
todoText: ''
})
this.id += 1
}
render() {
const { itemsList, todoText } = this.state
return (
<div>
<nav id="Home" className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#Home">Todo List</a>
</nav>
<main>
<div className='create-item'>
<input type="text" name="input-item" id="input-item" value={todoText} onChange={this.handleChange} placeholder="請輸入項目名稱" />
<button type="button" className="add-btn btn btn-primary btn-sm" onClick={this.addTodo}>Add</button>
</div>
<div className="Todo-items">
<ul>
{itemsList.map(todo => (
<li className='item'>
<input type='checkbox' />
<span className={`item-name ${todo.checked ? 'checked' : ''}`}>{todo.todo}</span>
<button className='delete-btn'>X</button>
</li>
))}
</ul>
</div>
</main>
</div >
)
}
}
```
上面這個是已經可以運作的新增功能,下面一一來說改了哪些地方:
1. 在第 7 行 `this.state` 增加了 `todoText` 狀態,只要 `todoText` 這個 `state` 的值改變,在第 43 行 `input` 表單的 `value` 值就會跟著改變,以確保 UI 畫面的表單裡面的值跟 `state` 裡面的值是同步的。
2. 第 14 行新增 `handleChange` 主要是用來改變 `todoText` 這個 `state` 的值,在第 43 行 `input` 表單放一個事件監聽 `onChange`,當表單一有變化就會呼叫 `handleChange` 把`todoText` 的值改為表單目前的值,每打一個字或刪除一個字, `todoText` 的值就會被改變一次。然後記得為 `handleChange` 加上第 10 行的 `this.handleChange = this.handleChange.bind(this)`,否則 `onChange` 事件監聽會呼叫不到 `handleChange`。
3. 最後就是第 20 行的 `addTodo`,主要負責新增 Todo 項目,然後在第 44 行的 `button` 按鈕新增事件監聽 `onClick`,當 `Add` 按鈕被按的時候就呼叫 `addTodo`。
在 `addTodo` 裡面的 `setState` 有使用 `[...itemsList]` 這個是展開運算子的用法,把原本 `itemsList` 變數裡面有的值全部攤開來,然後後面的 `{}` 才是我們要新增的 todo 項目的資訊,因此我們可以知道 `itemsList` 這個 Todo 所有項目的表單有三個 key,分別是 `id`、`checked`、`todo`。另外在第 28 行主要是清空 `todoText` 的值,這樣可以確保每次新增完項目後,`input` 表單的值會清空。然後第 31 行就是每次新增一個項目就會把 `id` 值加 1。
另外也要記得為 `addTodo` 加上第 11 行的 `this.addTodo = this.addTodo.bind(this)`。
綜合上述新增項目的功能,可以發現到使用 React 與使用 jQuery 來撰寫的差別是 jQuery 是**從表單的節點拿取表單的值**,然後使用 `push` 把表單的值新增到 `itemsList` 上面去。
但 React 則是從頭到尾都是藉由 `setState` 來拿表單上的值,把值放到 `state` 裡面儲存,接著哪邊需要表單的值,就從 `state` 裡面拿就可以了,簡單來說,就是把 `state` 當作一個管理員,所有資料變更的處理與資料的儲存都歸它管理了
### 刪除的功能
在做刪除功能之前,我們先拆解程式碼讓程式碼畫面更簡潔,因為目前在 `App` Component 裡的程式碼太多,所以新增一個檔案叫做 `todo.js`,把 `itemsList.map(todo => ()` 括弧裡面的所有標籤內容放進去 `todo.js` 檔案:
```jsx=
import React, { Component } from 'react'
class Todo extends Component {
constructor(props) {
super(props)
}
render() {
const { todo } = this.props
return (
<li className='item'>
<input type='checkbox' />
<span className={`item-name ${todo.checked ? 'checked' : ''}`}>{todo.todo}</span>
<button className='delete-btn'>X</button>
</li>
)
}
}
export default Todo
```
第 9 行的 `const { todo } = this.props` 就是引入在 `App` 的 `todo` 值。
而在 `app.js` 檔案需要新增`import Todo from './todo.js'` 來引進 `todo.js` 檔案,接著在 `itemsList.map(todo => ()` 裡面的括弧新增 `<Todo todo={todo} />`,意思就是把在 `todo.js` 檔案的 `Todo` Component 放進來 `App` 作為 Children Component,然後再在 `<Todo />` 標籤裡新增 `todo` 的 function 好讓 `toodo.js` 檔案的 `Todo` Component 可以使用 `this.props` 拿到 `map` 方法 `todo` 的值。
以下是修改過後的 `app.js` 檔案:
```jsx=
import React, { Component } from 'react'
import Todo from './todo.js' // 引入 todo.js
import './style.css'
class App extends Component {
constructor(props) {
super(props)
this.state = {
itemsList: [],
todoText: ''
}
this.id = 1
this.handleChange = this.handleChange.bind(this)
this.addTodo = this.addTodo.bind(this)
}
handleChange(e) {
this.setState({
todoText: e.target.value
})
}
addTodo() {
const { todoText, itemsList } = this.state
this.setState({
itemsList: [...itemsList, {
id: this.id,
checked: false,
todo: todoText
}],
todoText: ''
})
this.id += 1
}
render() {
const { itemsList, todoText } = this.state
return (
<div>
<nav id="Home" className="navbar navbar-light bg-light">
<a className="navbar-brand" href="#Home">Todo List</a>
</nav>
<main>
<div className='create-item'>
<input type="text" name="input-item" id="input-item" value={todoText} onChange={this.handleChange} placeholder="請輸入項目名稱" />
<button type="button" className="add-btn btn btn-primary btn-sm" onClick={this.addTodo}>Add</button>
</div>
<div className="Todo-items">
<ul>
{itemsList.map(todo => <Todo todo={todo} />)} // 放入 Todo Component
</ul>
</div>
</main>
</div >
)
}
}
export default App
```
接著我們開始做刪除的功能,先在 `App` Component 中新增 `deleteTodo` function:
```jsx=
deleteTodo(id) {
const { itemsList } = this.state
this.setState({
itemsList: itemsList.filter(todo => todo.id !== id)
})
}
```
這邊意思就是使用 `filter` 方法篩選,當傳入進來的 `id` 參數等同 `itemsList` 裡面的其中一個項目的 `id` 的時候,那就把那個項目從 `itemsList` 當中過濾掉(等同刪除)。(注意一樣要為 `deleteTodo` 加 `bind`)
再來更新 `<Todo />` 標籤:
```jsx=
<Todo todo={todo} deleteTodo={this.deleteTodo} />
```
新增一個 `deleteTodo` props 把 `App` 的 `deleteTodo` function 給傳下去讓 `Todo` Component 可以使用。
接著跳到 `todo.js`,新增 `delete` function:
```jsx=
delete() {
const { todo, deleteTodo } = this.props
deleteTodo(todo.id)
}
```
分別去拿兩個在 `App` 的 props(`todo`、`deleteTodo`),接著使用 `deleteTodo` props 呼叫在 `App` 的 `deleteTodo` function,並把 `todo.id` 值作為 function 的`id` 參數的引數,而 `todo.id` 就是當下按刪除的按鈕的那個元素對應到 `state` 的 `itemsList` 中所儲存的 `todo.id`。
最後在刪除按鈕的元素加上事件監聽:
```jsx=
render() {
const { todo } = this.props
return (
<li className='item'>
<input type='checkbox' />
<span className={`item-name ${todo.checked ? 'checked' : ''}`}>{todo.todo}</span>
// update here
<button className='delete-btn' onClick={this.delete}>X</button>
</li>
)
}
```
`onClick={this.delete}` 意思就是當刪除按鈕被按的時候,就呼叫 `delete` function。
這樣刪除按鈕就大功告成了,不過有個問題是每次新增項目的時候都會跑出一個 Warning 的提示:
![](https://i.imgur.com/gUgPJwD.png)
意思是要替每個在 `itemsList` 裡面的每個項目都加上 `key` 的 props 會比較好,對於 React 來說,陣列的每個項目最好都要有個獨立的 `key` 值代表那個項目,會比較方便操作。
所以我們只要在 `app.js` 檔案的 `<Todo />` 當中加上 `key` props 就可以了:
```jsx=
<Todo key={todo.id} todo={todo} deleteTodo={this.deleteTodo} />
```
### 標註已完成、未完成的功能
這個功能要加上的程式碼和刪除功能大同小異,只是篩選 `itemsList` 的方式不同。
首先在 `app.js` 檔案加上 `markTodo` function:
```jsx=
markTodo(id) {
const { itemsList } = this.state
this.setState({
itemsList: itemsList.map(todo => {
if (todo.id !== id) {
return todo
}
return {
...todo,
checked: !todo.checked
}
})
})
}
```
這邊在 `setState` 的做法就是用 `map` 方法掃遍每個 todo 項目,如果 `id` 不符合傳進來的參數的 `id` 值,那麼就原封不動把那一個正在掃的 todo 項目的內容傳回去;如果 `id` 符合的話,就會進入第二個 `return`,把 `checked` 這個 key 原先的值反過來,也就是原本是 `true` 的話那就把它改為 `false` and vice versa。這和最原先還沒用 React 的 `main.js` 檔案的寫法不同,這樣的寫法比較簡潔一些。
接著更新 `<Todo />` 多一個 `markTodo` 的 props:
```jsx
<Todo key={todo.id} todo={todo} deleteTodo={this.deleteTodo} markTodo={this.markTodo} />
```
再來看到 `todo.js`,新增一個 `mark` function:
```jsx
mark() {
const { todo, markTodo } = this.props
markTodo(todo.id)
}
```
這個 `mark` 就是負責 call `App` 的 `markTodo` function 並把 `id` 值傳回去。
然後我這邊掛事件監聽在 表單的 `checkbox` 上:
```jsx=
<input type='checkbox' onClick={this.mark} />
```
這樣標註功能就大功告成了。
### 加上 localStorage
這個範例最後一個步驟我們來把原本 todo list 有使用的 `localStorage` 給加到 React 上面。
只需要兩個 React 的 function:
1. `componentDidUpdate`
2. `componentDidMount`
首先,把 `itemsList` 的資料放到 localStorage 只要使用 `componentDidUpdate` 即可,也就是當狀態變更的時候(`itemsList` 的資料變更)就會存到 localStorage 去,因此只要這麼寫即可:
```jsx=
componentDidUpdate(prevProps, prevState) {
if (prevState.itemsList !== this.state.itemsList) {
window.localStorage.setItem('todoapp', JSON.stringify(this.state.itemsList))
}
}
```
接著如果要撈出 localStorage 存放的資料的話,只要使用 `componentDidMount` 即可,也就是當每次 render DOM 後就撈出資料。程式碼如下:
```jsx=
componentDidMount() {
const todoData = window.localStorage.getItem('todoapp')
if (todoData) {
const prevItemList = JSON.parse(todoData);
this.setState({
itemsList: prevItemList,
})
// 沒任何一筆資料的時候就不更新 this.id,維持 this.id = 1
if (prevItemList[prevItemList.length - 1] !== undefined) {
this.id = prevItemList[prevItemList.length - 1].id + 1
}
}
}
```
### checkbox 的同步
在最一開始實作 React 的時候,render UI 有一個 `<input type='checkbox' checked />` 的節點當時是說有問題,`checked` 的判定式沒有弄出來,所以現在這個段落要解決這個 bug,否則 checkbox 的打勾無法與 `itemsList.checked` 同步。
我先使用 `<input type='checkbox' checked={todo.checked} />`,它跑出了這段 Warning:
![](https://i.imgur.com/reUkC8N.png)
上面說如果你使用 `checked` 就要使用 `onChange` 不然 read-only 而已。如果你要更改 `checked` 的值的話,就要使用 `defaultChecked`。
因此改成 `<input type='checkbox' defaultChecked={todo.checked} />` 之後,就正常了!