React實作

實作Todo App

Jacy Chu
12 min readSep 7, 2023

從實作中,學習React。

參考影片:Learn React With This One Project

(涵蓋vite, className, hooks(useState&useEffect), components, props)

建立一個 React Todo App,完成品如下:

此篇文章架構:

  1. step by step 打造 Todo app
  2. 拆分成components
  3. 部署至GitHub

Step by step 打造 Todo app

step1 使用 Vite 建構專案

Vite是一種新型前端構建工具,能夠顯著提升前端開發體驗。它主要由兩部分組成:
一個開發服務器,它基於原生ES 模塊提供了豐富的內建功能,如速度快到驚人的模塊熱更新。
一套構建指令,它使用Rollup打包你的代碼,可輸出用於生產環境的高度優化過的靜態資源。

在終端機數入:

npm create vite@latest

> 選擇React
> Project name 輸入 react-todo-app
> 選擇JavaScript
> cd react-todo-app
> npm install
> npm run dev

此時 http://localhost:5173/ 畫面出現:

生成的專案架構如下:

main.jsx檔案

step2 加入 css 檔案

由於 css 部分非此練習重點,可點擊此以取得 style.css 檔案,下載加至src資料夾中。
在 App.jsx 檔案上方加入

import './style.css'

step3 加入JSX

將原本的 App.jsx 內容移除,加上下方程式碼:

App.jsx

從程式碼中可看到把 list 列出三項(task1, task2, task3) 因為上個步驟已加入 style.css,所以目前畫面如下:

step4 加入React icon

React Icon 可以參考 https://react-icons.github.io/react-icons

於終端機輸入指令安裝套件

npm install react-icons --save

找尋到適合的icon

在 App.jsx 上方 import 要用到的icon

import {FaTrashAlt} from 'react-icons/fa'

<button>Delete</button>換成<button><FaTrashAlt /></button>

原本的 Delete 字樣成功換成垃圾桶的 icon了

step5 加入useState

又稱作 State Hook ,實現在 Function Component 中使用 State 狀態的一個方法。

在 React 16.8 之前,若想使用到生命週期的方法或狀態 (state),只能使用 React 的 class component。但 React hooks 的出現,讓開發者使用 functional component時,也能夠操作 React 的功能和狀態。在 React 中,用 use 開頭的函式稱之為 Hook,有些 Hooks 是 React 內建的功能,例如:useStateuseEffect

Hook的規則:
1.只能在最頂層使用 Hook(不能在 for 循環、if…else 或巢狀中(如: map )中使用 Hook)
2.只能在 React 函式中使用 Hook

採用 JavaScript ES6 解構賦值的方式來宣告兩個變數,第一個變數放state變數名稱,第二個變數放要setState() 這個函式的名稱。

useState(“20%”)是代表初始值。

React 當中,更新 state 時要用 immutable (不可改變)的寫法。因為物件是傳址 (pass by reference),如果直接改物件本身,該物件記憶體的位置沒有變,React會不知道該物件有改,即不會使用新的值去渲染畫面造成錯誤。

在 App.jsx 檔案上方加入

import { useState } from 'react'

function App中加入

const [newItem, setNewItem] = useState('task123')

並在 <input> 加上 value={newItem} 此時畫面會看到:

由於目前的預設 input 是 task123 實際上是由使用者輸入

所以把 useState 改成 useState('')

<input> 加上 onChange={e => setNewItem(e.target.value)}

當使用者操作input框,有修改時會響應在畫面上。

step6 加入todos邏輯

function App中加入

const [todos, setTodos] = useState([])

寫一個 handleSubmit function

function handleSubmit(e) {
e.preventDefault()
setTodos((currentTodos) => {
return [
...currentTodos,
{id: crypto.randomUUID(), title: newItem, completed: false}
]
})
setNewItem('')
}

這邊的 setTodos 內代入 currentTodos 是當有使用到“目前的值”就要代入

setNewItem('') 是把輸入框的內容清空

在 Add button 上加上 onCLick={handleSubmit}

在<ul> tag中

把原本寫死的<li>們註解掉(之後可以刪掉)

{/* <li>
<label>
<input type="checkbox"/>
task 1
</label>
<button className='trash-icon'><FaTrashAlt /></button>
</li> */}

{todos.map(todo => {
return (
<li key={todo.id}>
<label>
<input type="checkbox" checked={todo.completed}/>
{todo.title}
</label>
<button className='trash-icon'><FaTrashAlt /></button>
</li>
)
})}

使用map去生出todos

加上 toggle checkbox

label中 <input> 加上 onChange={e => toggleTodo(todo.id, e.target.checked)}

寫一個 toggleTodo function

  function toggleTodo(id, completed) {
setTodos(currentTodos => {
return currentTodos.map(todo => {
if (todo.id === id) {
return { ...todo, completed }
}
return todo
})
})
}

加上 delete function,先實作單個 delete

delete btn上加上 onClick={() => deleteTodo(todo.id)}

(這邊不能改寫成 onClick={deleteTodo(todo.id)},因為這樣是直接回傳 deleteTodo(todo.id) 執行結果而非call deleteTodo function)

寫一個 deleteTodo function

  function deleteTodo(id) {
setTodos(currentTodos => {
return currentTodos.filter(todo => todo.id !== id)
})
}

再加上 deleteAll function

Delete All btn上加上 onClick={deleteAll}

  function deleteAll() {
setTodos([])
}

step7 加上No Todos

在 todo 區塊加上

{todos.length === 0 && "No Todos"}

todos.length 為true時,會出現 No Todos 字樣

拆分成components

拆分出NewTodoForm.jsx

新增一個jsx檔案,名為 NewTodoForm.jsx (component的結尾必須是jsx)

把App.jsx中 form部分搬移過去也把 handleSubmit function搬移過去

接著把 const [newItem, setNewItem] = useState('') 搬移過去
記得在 NewTodoForm.jsx 檔案 import useState

回到App.jsx檔案,在上方
import {NewTodoForm} from './NewTodoForm'

並在原本 App.jsx form的位置,加上 <NewTodoForm /> 元件

此時,NewTodoForm.jsx 中 handleSubmit function 會用到 todo,而App.jsx也會用到,此時需要用props去達成元件溝通。

在App.jsx檔案中新增 addTodo function

  function addTodo(title) {
setTodos((currentTodos) => {
return [
...currentTodos,
{id: crypto.randomUUID(), title: title, completed: false}
]
})
}

而NewTodoForm.jsx 中 handleSubmit function改成

  function handleSubmit(e) {
e.preventDefault()
if (newItem === "") return
props.onSubmit(newItem)
setNewItem('')
}

props 用於父傳子組件溝通

App.jsx

其中onSubmit跟onDeleteAll是用來傳遞function

NewTodoForm.jsx

傳入props即可使用

拆分出TodoList.jsx & TodoItem.jsx

參考上述做法,拆分 component。

App.jsx

TodoList.jsx

TodoItem.jsx

此時,沒有存在 local storage,要存在local storage可以藉由 useEffect 協助。有關useEffect可以參考這篇:React介紹2,其中的生命週期部分。

在 App.jsx 檔案中加入

當todos改變時,localStorage隨之改變。新增刪除todo項目,按下重新整理按鈕時,資料不會不見了。

部署至GitHub

參考這支影片:

由於使用vite建構app,在package.json中deploy部分改成:

"deploy": "gh-pages -d dist"

在部署過程,若不能順利使用Https推上code,可以參考這篇使用SSH推東西上github。

部署結束,點開網址沒有看到內容可以參考這篇:

修改完config檔案即完成

React 實作小結

附上 react-todo-app 專案的 GitHub 檔案連結 🔗 ,專案資料夾名稱:react-todo-app 。有任何指點或建議都可以在下方留言~🙏

--

--