本文使用的是 Redux Toolkit。
架構模式 作用: 透過一個單獨的 state 對象保存整個應用的狀態,這個 state 對象不能被直接修改,而是當某些數據發生變化後,透過 actions 及 reducers 來創建新的對象。
流程:
Redux 下載: npm install @reduxjs/toolkit
官方: Redux Toolkit is our official, opinionated, batteries-included toolset for efficient Redux development. It is intended to be the standard way to write Redux logic, and we strongly recommend that you use it.
項目結構
1 2 3 4 5 6 7 8 9 10 11 |- package.json |- ... |- public |- src -|- index.js | |- App.js | |- routes.js | |- ... | |- redux -|- store.js | |- reducers - todoReducer.js | |- actions - todoAction.js | |- constants - todo.js
功能: 當 store 內儲存的狀態改變時,使用該狀態的組件資料會相應改變。
代碼範例講解
透過 createStore(reducer) 讓資料能夠統一管理。
1 2 3 4 5 import { createStore } from 'redux' ;import todoReducer from './reducers/todoReducer' ;let store = createStore(todoReducer);export default store;
創建常量值(目的是避免名稱寫錯)
1 2 3 4 5 export const ADD_TODO = 'add_todo' ;export const DELETE_TODO = 'delete_todo' ;export const GET_TODO = 'get_todo' ;
創建 reducer 的執行內容
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 import { ADD_TODO, DELETE_TODO, GET_TODO } from '../constants/todo' ;const initialState = [ { userId : 1 , id : 1 , title : 'delectus aut autem' , completed : false , }, ]; export default function todoReducer (preState = initialState, action ) { let newState = [...preState]; switch (action.type) { case ADD_TODO: newState = [...preState, action.payload]; break ; case DELETE_TODO: newState = newState.filter((item ) => item.id !== action.payload.id); break ; case GET_TODO: break ; default : break ; } return newState; }
自動化生成 action 值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import { ADD_TODO, DELETE_TODO } from '../constants/todo' ;export function addTodo (payload ) { return { type : ADD_TODO, payload, }; } export function deleteTodo (payload ) { console .log('@action' , payload); return { type : DELETE_TODO, payload, }; }
讓需要使用儲存庫(store)的組件訂閱資料庫
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import { Link, useMatch, Navigate, Outlet } from 'react-router-dom' ;import store from './01.redux/store' ;export default function App ( ) { store.subscribe(() => console .log('@App訂閱的資料更新啦' , store.getState())); return ( <div className ='App' > {/* 取得 state 的數據*/} <Link to ='/home' > 首頁</Link > <Link to ='/search' > 搜索</Link > {useMatch('/') ? <Navigate to ='/home' replace ={true} /> : <Outlet /> } </div > ); }
使用 store ,並透過 dispatch 觸發更新,並使用 useState 來做組件的資料管理以便更新視圖。
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 34 35 36 37 38 39 40 41 import { useState } from 'react' ;import store from './01.redux/store' ;import { deleteTodo } from './01.redux/actions/todoAction' ;export default function Home ( ) { const [todo, setTodo] = useState(store.getState()); store.subscribe(() => { console .log('@home訂閱的資料更新啦' ); setTodo(store.getState()); }); const deleteItem = (item ) => { store.dispatch(deleteTodo(item)); }; return ( <div > 我是Home,放置所有 todo 資料 <hr /> {todo.map((item) => ( <li key ={item.id} > {item.title} <button onClick ={(e) => { deleteItem(item); }}> 刪除 </button > </li > ))} </div > ); }
redux-thunk 異步更新 state 先用一個錯誤寫法 說明: 直接使用異步函數,並無法正確產生 action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 import { GET_TODO } from '../constants/todo' ;export function getTodos ( ) { fetch('https://jsonplaceholder.typicode.com/todos/10' ) .then((res ) => res.json()) .then((json ) => { return { type : GET_TODO, payload : json, }; }); }
正解 :使用 redux-thunk
redux-thunk 工作流程
先下載
使用 redux-thunk,並讓 redux-thunk 與 redux 產生關聯(applyMiddleware)
1 2 3 4 5 6 7 import { applyMiddleware, createStore } from 'redux' ;import todoReducer from './reducers/todoReducer' ;import thunkMiddleware from 'redux-thunk' ;let store = createStore(todoReducer, applyMiddleware(thunkMiddleware));export default store;
action 中透過返回異步函數,讓 redux-thunk 知道他要開始工作了
1 2 3 4 5 6 7 8 9 10 11 12 export function getTodos ( ) { console .log('@action getTodos 1' ); return async (dispatch, getState) => { const res = await fetch('https://jsonplaceholder.typicode.com/todos' ); const todos = await res.json(); console .log('@action getTodos 2' , todos); dispatch({ type : GET_TODO, payload : todos }); }; }
在組件中使用的方式跟一般觸發更新 store 相同,都是用 dispatch
1 2 3 4 5 6 7 8 9 10 11 useEffect(() => { if (store.getState().length === 0 ) { console .log('@home 發出數據請求' ); store.dispatch(getTodos()); } else { console .log('@home緩存' ); } }, []);
多個 reducer 的載入方法
創建新的 reducer。(actionCreator 可創可不創)
1 2 3 4 5 6 7 8 9 10 11 12 import { SEARCH_TEXT } from '../constants/todo' ;const initialValue = '' ;export default function searchReducer (preState = initialValue, { type, payload } ) { switch (type) { case SEARCH_TEXT: return payload; default : return '' ; } }
在 store.js 中使用 combineReducers 把多個 reducer 做整合用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import { createStore, combineReducers } from 'redux' ;import todoReducer from './reducers/todoReducer' ;import searchReducer from './reducers/searchReducer' ;const reducers = combineReducers({ todos : todoReducer, searchText : searchReducer, }); let store = createStore(reducers);export default store;
組件中使用,一樣透過 store.getState() 取得所有狀態,不過多個 reducers 的狀態呈現會是一個物件。
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 import { useMemo, useState } from 'react' ;import store from './01.redux/store_combineReducers' ;export default function SearchItem ( ) { const [searchRes, setSearchRes] = useState(store.getState().searchText); store.subscribe(() => { console .log('@SearchItem 訂閱資料' , store.getState()); setSearchRes(store.getState().searchText); }); const searchTodos = useMemo( () => store.getState().todos.filter((todo ) => todo.title.includes(store.getState().searchText)), [searchRes] ); return ( <ul > {searchTodos.length ? ( searchTodos.map((todo) => <li key ={todo.id} > {todo.title}</li > ) ) : ( <li > 無搜索資料</li > )} </ul > ); }
redux 的開發者工具 redux-devtools
(Chrome 版本) 在線上應用程式商店下載 Redux DevTools
使得專案可以使用 Redux DevTools。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import { applyMiddleware, createStore, compose } from 'redux' ;import todoReducer from './reducers/todoReducer' ;import thunkMiddleware from 'redux-thunk' ;const composeEnhancers = window .__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;let store = createStore( todoReducer, composeEnhancers(applyMiddleware(thunkMiddleware)) ); export default store;
如此一來就可以更直觀的觀察到 redux 數據的變化。