從實作 To Do List 理解 useContext 搭配 useReducer 運作模型—(附圖)(中)
2022-11-16 Wed
本文將以實作 To Do List 理解 Listing State Up 包含以下部分
- useContext 運作模型—圖解
- useReducer 運作模型—圖解
- 資料夾結構
- 具體程式碼
為了解決需要透過層層元件傳遞,因此可以使用 context 將 status 統一管理,透過訂閱的方式將所需的 status 取出,就可以避免僅是為了父傳遞給子元件的子元件而必須在第一層的子元件的 props 當中接棒傳給第二層子元件。
useContext 運作模型—圖解
大致可以理解成下圖的方式
useReducer 運作模型—圖解
另外將處理的邏輯抽離出來到 reducer,component 僅做派送 action 物件給 reducer 最後回傳給訂閱的 component status 大致如下圖
- action creator 僅製作 action 物件
- dispatch 負責派送 action 物件
- reducer 接收 action 物件和 status
- status 依據所訂閱的 component 渲染
資料夾結構
│ App.js
│ index.js
│
├─components
│ └─Todo
│ AddToDo.jsx
│ index.jsx
│ ListToDo.jsx
│ ToDoItem.jsx
│
└─store
toDoActionCreator.js
toDoContext.js
toDoReducer.js
我們將建立一個store 資料夾用來儲存 handler 的邏輯以及 listData 的 state,其中包含三個檔案toDoActionCreator、toDoContext、toDoReducer
程式碼部分
toDoContext 檔案
這個檔案僅用來建立一個 context,稍後將會引入至 App 的檔案裡
import { createContext } from "react";
//用來建立一個 context
const toDoContext = createContext();
export default toDoContext;
toDoActionCreator 檔案
這邊的 function 是用來製造 action 物件,其物件裡面包含要做的動作類型 (type) 和輸入值 (payload)
export function addToDo(input) {
return {
type: 'ADD_TO_DO',
payload: input
}
}
export function deleteToDo(id) {
return {
type: "DELETE_TO_DO",
payload: id
}
}
export function completeToDo(id) {
return {
type: "COMPLETE_TO_DO",
payload: id
}
}
toDoReducer 檔案
reducer 函式的參數接收原先的 state 和要執行的 action 需要注意的是如果沒有對應的 action 的時候應當回傳原先的 state。
程式部分可以看到這邊使用 switch case 來分門別類 action 物件。使用 default 用來作為當傳進來的 action 沒有對應的時候回傳原先的 state,
function toDoReducer(state, action) {
switch (action.type) {
case 'ADD_TO_DO':
if (action.payload === "") { return state; }
return [
...state,
{
content: action.payload,
id: Date.now(),
done: false
}
];
case 'DELETE_TO_DO':
return state.filter(
toDoItem => toDoItem.id !== action.payload
)
case 'COMPLETE_TO_DO':
return state.map(item => {
if (item.id === action.payload) {
item.done = !item.done;
}
return item;
})
default:
return state;
}
}
export default toDoReducer;
App 檔案
最後我們在 app 檔案引入 toDoContext 和 toDoReducer,接下來將宣告 listData state 並將透過 context 發布至底下的 component
import React, { useReducer } from 'react'
import { AddToDo, ListToDo } from './components/Todo';
import toDoContext from './store/toDoContext';
import toDoReducer from './store/toDoReducer';
const App = () => {
//這邊給定了初始狀態將其發布
const initialState = [
{
content: "測試",
id: 409823109843,
done: false
},
{
content: "測試二",
id: 543098209,
done: false
}
];
// 透過 Provider 將 listData 和 dispatch 發佈
const [listData, listDispatch] = useReducer(toDoReducer, initialState);
return (
<div >
<toDoContext.Provider value={{ listData, listDispatch }}>
<AddToDo />
<ListToDo />
</toDoContext.Provider>
</div>
)
}
export default App
AddToDo 檔案
這裡先引入 useContext,將 toDoContext 所建立的context 給放入到 useContext 的參數中,就能簡單的提取出裡面的值,這邊我們提取 listDispatch,另外也要引入 ActionCreator,其預計用來製造 action 的物件,將 listDispatch 放到 onClick 事件函式當中,當按下 add 的時候就會使用 listDispatch 分派 action,其透過接收 input 參數給 actionCreator 作為 payload 的用途。
import { useState, useContext } from 'react';
import toDoContext from '../../store/toDoContext';
import { addToDo } from '../../store/toDoActionCreator';
const AddToDo = () => {
//in-line style 的部分
const margin0Auto = { width: "300px", margin: "0 auto" };
const textAlign = { textAlign: "center" };
//使用 Controlled component
const [input, setInput] = useState("");
const inputChange = (e) => {
setInput(e.target.value);
}
//提取 listDistpatch 以便稍後用來發送事件
const { listDispatch } = useContext(toDoContext);
return (
<div style={{ ...textAlign, ...margin0Auto }}>
<input type="text" value={input} onChange={inputChange} />
<button onClick={() => {
listDispatch(
addToDo(input)
)
setInput('');
}}>add</button>
</div>
)
}
export default AddToDo
ListToDo 檔案
同樣引入 toDoContext 為了提取儲存在 store 的 status,另外也引入 useContext 的 hook 來簡化寫法,這時候我們就能得到 listData。
import React, { useContext } from 'react'
import ToDoItem from './ToDoItem';
import toDoContext from '../../store/toDoContext';
const ListToDo = () => {
//in-line style 的部分
const margin0Auto = { width: "300px", margin: "0 auto" };
//使用 useContext 提取 listData
const { listData } = useContext(toDoContext);
return (
<ul style={margin0Auto} >
{listData.map((data) => {
return <ToDoItem key={data.id} content={data.content} id={data.id} done={data.done} />
})}
</ul >
)
}
export default ListToDo
ToDoItem 檔案
引入 toDoContext 將其放入 useContext 提取出 listDispatch,同樣引入 actionCreator 來製造完成和刪除的 action 物件,其參數最後會變成 action 物件的 payload
import React, { useContext } from 'react'
import toDoContext from '../../store/toDoContext';
import { deleteToDo, completeToDo } from '../../store/toDoActionCreator';
const ToDoItem = ({ id, content, done }) => {
const margin10 = { margin: "10px" };
const displayFlex = { display: "flex", justifyContent: "center", alignItems: "center" };
const displayBlock = { display: "block" };
const { listDispatch } = useContext(toDoContext);
return (
<li style={{ ...margin10, ...displayFlex }}>
<input type="checkbox"
checked={done}
onChange={
() => listDispatch(completeToDo(id))
} />
<p style={
{ textDecoration: done ? 'line-through' : 'none' }
}
>
{content}
</p>
<button style={{ ...margin10, ...displayBlock }}
onClick={
() => listDispatch(deleteToDo(id))
} >
delete
</button>
</li >
)
}
export default ToDoItem
最後應當可以看到畫面如下,其中會先出現兩個 toDo 是因為我們先前在 App 那支檔案有宣告過 initialState