讓我們重新回顧過去的內容
從頭來做個簡單的 ToDo List 來練習吧!
建立 React App
安裝 node, yarn 的部分讓我們跳過,不太懂的可以參考《1. 建立良好的開發與執行環境》
yarn create react-app --template typescript demo-app
建立專案架構
通常 React 會需要建立多個頁面,建議放在相同資料夾
所以我們先在 pages 裡面建立一個 HomePage.tsx
// src/pages/HomePage.tsx
import React from 'react'
const HomePage: React.FC = () => {
return <div>HomePage</div>
}
export default HomePage
接著為了讓 React App 能夠順利實作多頁面
我們引入 react-router(-dom) 的套件(因為沒有內建型別檔案,所以需要額外安裝)
yarn add react-router react-router-dom
yarn add --dev @types/react-router @types/react-router-dom
接下來我們改寫 App.tsx
// src/App.tsx
import React from 'react'
import { Route, Switch } from 'react-router'
import { BrowserRouter } from 'react-router-dom'
import HomePage from '../pages/HomePage'
const App = () => {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={HomePage} />
</Switch>
</BrowserRouter>
)
}
export default App
接著測試看看是否正常執行,執行 yarn start
應該會出現以下畫面
撰寫元件展示
我們接下來撰寫 ToDo List 需要的元件
不過在那之前,我們可以先引用 UI Library 來快速開發
這邊使用的是 Chakra UI(最近很喜歡的一個 UI Library)
你也可以使用 react-bootstrap
, @material-ui/core
, antd
等等
yarn add @chakra-ui/core @emotion/core @emotion/styled emotion-theming
由於 Chakra 本身有內建型別定義檔案,所以我們不需要再額外安裝
不過這邊需要修改 App.tsx
來設定主題:
// src/App.tsx
import { theme, ThemeProvider } from '@chakra-ui/core'
import React from 'react'
import { Route, Switch } from 'react-router'
import { BrowserRouter } from 'react-router-dom'
import HomePage from '../pages/HomePage'
const App = () => {
return (
<ThemeProvider theme={theme}>
<BrowserRouter>
<Switch>
<Route exact path="/" component={HomePage} />
</Switch>
</BrowserRouter>
</ThemeProvider>
)
}
export default App
接著我們可以開始建立元件了!
首先我們先在 components 的資料夾裡面新增 todo/ 和 shared/ 兩個資料夾
正如之前文章所述,components 內建議以「功能」進行管理
todo/ 用來放置所有跟 todo list 有關的功能
shared/ 用來放置共用的功能
在未來,若是想要新增「提醒功能」,我們則可以直接新增 notification/ 即可
我們先來定義型別,建議建立 types/ 資料夾,並創建 index.d.ts
// types/index.d.ts
type TodoStatus = 'TODO' | 'DOING' | 'DONE'
這邊只有定義 status,可以視情況新增其他型別
再來我們建立 todo item,這邊將 icon 的部分抽到 utility 那邊
另外,為了讓這個元件有更高擴充性,所以我們使用 children
來渲染內容
以及將 remove 的事件交由父層來處理(使用 onRemove props)
// src/components/TodoItem.tsx
import { ListIcon, ListItem } from '@chakra-ui/core'
import React from 'react'
import { getIconFromTodoStatus } from '../../utils'
type TodoItemProps = {
status: TodoStatus
onRemove?: () => void
}
const TodoItem: React.FC<TodoItemProps> = ({ children, status, onRemove }) => {
return (
<ListItem>
<ListIcon icon="close" color="black.500" onClick={() => onRemove && onRemove()} />
<ListIcon icon={getIconFromTodoStatus(status)} color="green.500" />
{children}
</ListItem>
)
}
export default TodoItem
注意一下~ utility 建議為純 TypeScript,不建議在裡面使用 tsx
// src/utils.ts
export const getIconFromTodoStatus = (status: TodoStatus) => {
switch (status) {
case 'TODO':
return 'time'
case 'DOING':
return 'spinner'
case 'DONE':
return 'check'
default:
return 'close'
}
}
接著,我們來撰寫 Input,這邊我們使用 Controlled Component 寫法
我們將會把 submit 事件交給父層處理(使用外部給的 props)
import { Button, Input, InputGroup, InputRightElement } from '@chakra-ui/core'
import React, { useState } from 'react'
type TodoInputProps = {
onSubmit?: (title: string) => void
}
const TodoInput: React.FC<TodoInputProps> = ({ onSubmit }) => {
const [value, setValue] = useState('')
const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = e => setValue(e.currentTarget.value)
return (
<InputGroup>
<Input value={value} onChange={handleInputChange} type="text" placeholder="我等等要做..." />
<InputRightElement width="4.5rem">
<Button isDisabled={!value} h="1.75rem" size="sm" onClick={() => onSubmit && onSubmit(value)}>
送出
</Button>
</InputRightElement>
</InputGroup>
)
}
export default TodoInput
撰寫資料處理
為了專案可以擴充,我們這邊會以較大專案的視角來撰寫
資料部分我們使用 localstorage 的暫存
有了 todo item 之後,我們必須要拿到所有 item 的資料才行
因此,我們使用 custom hook 來完成資料的處理
一樣是放到 hooks/ 資料夾,使用功能來命名
注意:custom hook 也是純 TypeScript,不要使用 tsx
// hooks/todo.ts
import { useState, useCallback, useEffect } from 'react'
export type TodoItem = {
id: string
title: string
status: TodoStatus
}
export type TodoList = TodoItem[]
// fake data hook
export const useTodoList = () => {
const [todoList, setTodoList] = useState<TodoList>([])
const fetchTodoList = useCallback(() => {
const data = localStorage.getItem('todoList')
const parsedData: TodoList = data ? JSON.parse(data) : []
setTodoList(parsedData)
}, [])
const addTodoItem = useCallback(
(title: string, status: TodoStatus) => {
const newTodoItem = {
id: Date.now().toString(),
title,
status,
}
const newTodoList = [...todoList, newTodoItem]
localStorage.setItem('todoList', JSON.stringify(newTodoList))
setTodoList(newTodoList)
},
[todoList],
)
const removeTodoItem = useCallback(
(todoItemId: string) => {
const idx = todoList.findIndex(todoItem => todoItem.id === todoItemId)
const newTodoList = [...todoList.slice(0, idx), ...todoList.slice(idx + 1)]
localStorage.setItem('todoList', JSON.stringify(newTodoList))
setTodoList(newTodoList)
},
[todoList],
)
useEffect(() => fetchTodoList(), [fetchTodoList])
return {
todoList,
fetchTodoList,
addTodoItem,
removeTodoItem,
}
}
我們將資料狀態存在這個 custom hook 裡面
並且提供 addTodoItem, removeTodoItem 來讓外部使用
完成後我們就可以組裝到 HomePage
這邊會使用 hook 裡面的資料以及函式
import { List } from '@chakra-ui/core'
import React from 'react'
import TodoInput from '../components/todo/TodoInput'
import TodoItem from '../components/todo/TodoItem'
import { useTodoList } from '../hooks/todo'
const HomePage: React.FC = () => {
const { todoList, addTodoItem, removeTodoItem } = useTodoList()
return (
<div>
<TodoInput onSubmit={title => addTodoItem(title, 'TODO')} />
<List>
{todoList.map((todoItem, idx) => (
<TodoItem
key={idx}
status={todoItem.status}
onRemove={() => {
removeTodoItem(todoItem.id)
}}
>
{todoItem.title}
</TodoItem>
))}
</List>
</div>
)
}
export default HomePage
這樣簡易版的 Todo List 就完成了!
這邊需要提醒一下,因為我們的 todoList 狀態是在 custom hook 維護
如果在 HomePage 以外的其他地方也使用 const { todoList} = useTodoList()
那麼將不會更新到最新的狀態,除非重新 fetchTodoList()
結語
這篇實在是好難寫,真心佩服能把 step by step 寫得很詳細的人
這邊附上此 demo 的 github 連結:https://github.com/kkshyu/demo-app
裡面用上許多我們嘗試了很多次才使用的 pattern(像是 App.tsx 應該放在 components/ 裡面)
雖然沒有說明的很仔細,希望之後能開個分享會來分享