本文為 Lidemy [FE303] React 的好夥伴:Redux > Redux 核心:Middleware 的筆記內容
零、redux 的最後一塊拼圖: middleware
懶人工具包:Redux Toolkit
仲介:middleware
- 函式處理仲介:redux thunk 讓 action 變身成函式,不只是物件
一、Redux Toolkit 簡稱 RTK
1. 為什麼要用它?
因 redux 基本設置(boilerplate)過多,寫起來很煩,因此 Redux Toolkit 就是把 Redux 包裝好,讓你不用自己去處理 store 那些的一個工具包,有點懶人包的概念。
2. 要怎麼安裝?
a. 結論:跟官網不同的是,我用的是 $ npx create-react-app@latest blog --template redux
,這個會一次把 react/redux/redux toolkit/react-redux 都裝好
b. 安裝過程
b.1 指令 $ npx create-react-app my-app --template redux
一直報錯,就把 vs code 刪掉重裝了。
b.2 重裝沒效,最後用了 $ npx create-react-app@latest blog
雖然跑得起來,可是那是純 react,所以現在要跑的話要用下面的:
b.3 $ npx create-react-app@latest blog --template redux
就可以一次把 react/redux/redux toolkit/react-redux 都裝好
c. 檔案導覽
裡面的 store、Provider 都幫你設置好了
createSlice 就像一個完整的功能: 裡面結合了 action, reducer, actionTypes
- toolkit 底層用了 immer 這個套件能讓我們直接更改狀態,不用再寫什麼 ...state 回傳一個新的狀態之類的
// Redux Toolkit allows us to write "mutating" logic in reducers. It
// doesn't actually mutate the state because it uses the Immer library,
// which detects changes to a "draft state" and produces a brand new
// immutable state based off those changes
- toolkit 把 aciton 跟 actionTypes 結合在一起,變成一個函式
參考 Reducers and actions 下面的 methods createReducer()
二、Redux middleware
又稱中間件,是一個仲介的概念;看起來常用在「非同步的事件處理上」,例如:呼叫 API 或者處理登入事件等
我的理解:middleware 很像是交通仲介,例如說我有一個 action 是從 A 地到 B 地,那 middleware 幫我做的事可能是「買車票」
參考資料:Thunks and Async Logic、詳解 Redux Middleware
三、Redux thunk 把物件般的 action 變成函式
Redux thunk 是一個執行函式的 middleware,把 action 變成函式,並且執行這個函式。
四、把 blog 加上 redux
?1. 整理資料夾結構
components folder
App folder
NavBar folder
pages
- 各式各樣的頁面們.js
redux
reducers folder
- postReducer.js
store.js
contexts.js
index.js
utils.js
WebAPI.js
註:解決的問題包含沒有裝 react-router-dom 以及檔案路徑問題(用越多工具就要裝來裝去,難怪需要工具管理程式來管理)
2. 02'30" 改寫單一文章功能
a. 實作步驟:
a.1 創建 store
// store.js
import { configureStore } from '@reduxjs/toolkit';
import postReducer from './reducers/postReducer';
export default configureStore({
reducer: {
posts: postReducer,
},
});
a.2 創建 reducers 資料夾 > postReducer.js
// postReducer.js
import { createSlice } from '@reduxjs/toolkit';
import { getArticle } from '../../WebAPI';
// 定義 reducer 裡面的行為
export const postReducer = createSlice({
name: 'posts',
initialState: {
isLoadingPost: false,
// 這裡設為 null 會報錯:Uncaught TypeError: Cannot read properties of null
// 正確寫法:post: "",
post: null,
},
reducers: {
setIsLoadingPost: (state, action) => {
state.isLoadingPost = action.payload;
},
setPost: (state, action) => {
state.post = action.payload;
},
}
});
export const { setIsLoadingPost, setPost } = postReducer.actions;
// call API
export const getPost = (id) => (dispatch) => {
dispatch(setIsLoadingPost(true));
getArticle(id)
.then((res) => {
dispatch(setPost(res));
dispatch(setIsLoadingPost(false));
})
.catch((err) => {
console.log(err);
});
};
export default postReducer.reducer;
a.3 把剛剛在 reducer 定義的呼叫 API 的函式拿到 Article 頁面裡用
// ArticlePage.js
import { getPost } from '../../redux/reducers/postReducer';
import { useDispatch, useSelector } from "react-redux";
export default function ArticlePage() {
let { id } = useParams();
const dispatch = useDispatch();
const isLoading = useSelector((store) => store.posts.isLoadingPost);
const article = useSelector((store) => store.posts.post);
useEffect(() => {
dispatch(getPost(id));
}, [id, dispatch])
return <Article article={article} />;
}
註:要測試 react and redux 有沒有串起來,影片裡面用的是 console.log(isLoading)
,因為預設值是 false,所以如果印出的值把它改成 true 還是正常印出來,那就表示有抓到
b. error log
debug 第一個路徑就是開 redux 的 devtool,去看看有沒有發生你預期的行為,在這裡就是有沒有發送 API 去抓資料
ArticlePage.js:29 Uncaught TypeError: Cannot read properties of null (reading 'title')
都改寫好以後,出現這個錯誤訊息,發現 Article component 沒有抓到標題跟內容,照著影片嘗試了 console.log(isLoading)
發現有從 false 變成 true,因此確定有把 react redux 串起來,API 的部分也沒有錯
註:console.log(isLoading)
超可怕的,整個 devtool 就一直跳訊息,電腦燒起來了
嘗試:更改 props 名稱,但是發現不管什麼名稱都一樣,因為排除這個原因
嘗試:把 useEffect 拿掉,發現有時候可以運作有時不行,怪怪的
嘗試:把 cookie 跟紀錄全部清掉,但不確定有沒有影響
嘗試:後來發現是「不可以重新整理頁面,如果重新整理頁面,就會發現壞了,而這個應該跟我的頁面是 SPA 有關」,上述錯誤訊息還是存在,但是只要「不重新整理頁面」功能就是好的。
嘗試:reducer post 初始化狀態設為 "null" 是錯的,設為
""
就不會有這個錯了,這樣重新整理頁面、怎麼點擊跳轉都是正常的,太好了。
註:後來發覺用 []
比 ""
好
五、改寫發表文章功能
a. 實作步驟
新增一個 reducer 裡面的狀態 -> newPost 去抓 API -> 引入 dispatch & selector -> 當新增文章時,去呼叫 API -> 當成功新增文章時,就導到首頁
要解決的問題:新增文章跳轉到首頁以後,再按一次新增文章,會跳到上一篇新增文章單一頁面,但應該是新增文章輸入頁面才對
第一種:離開頁面時,把 newpostresponse 清空
useEffect(() => {
return () => {
dispatch(setNewPostResponse(null));
}
}, [dispatch])
第二種:
利用 redux thunk 能夠回傳一個 promise 的功能,直接拿到 newPostResponse 的值,連 selector 都省了
a. 原本在 reducer 抓 API 長這樣
export const newPost = (data) => (dispatch) => {
newArticle(data).then((res) => {
dispatch(setNewPostResponse(res));
});
};
b. 但現在在裡面回傳一個 promise
export const newPost = (data) => (dispatch) => {
return newArticle(data).then((res) => {
dispatch(setNewPostResponse(res));
return res;
});
};
c. 接著在頁面那邊把那個 promise 改寫
const handleNewArticle = () => {
setErrorMessage(null);
if (!title || !body) {
const errorMessage = "Please input missing field";
setErrorMessage(errorMessage);
} else {
dispatch(newPost({
title,
body
})).then((newPostResponse) => {
if (newPostResponse && newPostResponse.id) {
navigate("/posts/" + newPostResponse.id);
}
})
}
};
第三種:
利用 isLoadingNewPost 來判斷時間點,新增一個 isLoadingNewPost 的狀態,藉由判斷狀態來規範何時要導回「剛剛新增的文章頁面」
有一個情況就是「剛剛正在 loading,而現在沒有在 loading 了」
if (!isLoadingNewPost && prevIsLoadingNewPost)
-> 假設符合這個狀態,就表示已經拿到 response 了
作法:在 reducer 新增一個 isLoadingNewPost 的狀態,並且使用 usePrevious 這個別人做好的 hook,來實現這個做法
b. error log: VM4939:1 Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0
問題描述:無法新增文章,不會跳轉頁面;打開 redux devtool,沒有任何紀錄,react redux 沒有串起來
b.1 解法一:Unexpected token < in JSON at position 0
API 網址錯誤
把最後面的
.then((res) => res.json());
換成.then((res) => res.text());
這樣 redux dev tool 真的有去呼叫 newPost 那支 API 了,但是這個解法好奇怪
b.2 解法二:搞了半天是 WebAPI 裡面的寫法,需要的資料要解構語法,寫完以後總算有去呼叫 newPost 這個 API 了
然後再把 newArticlePage 裡面的名稱改好,要叫做 title & body 相關,不能用其他的
// 原本是寫成...(title, body)...
export const newArticle = ({ title, body }) => {
const token = getAuthToken();
return fetch(`${BASE_URL}/posts`, {
method: "POST",
headers: {
"content-type": "application/json",
authorization: `Bearer ${token}`,
},
body: JSON.stringify({
title,
body,
}),
}).then((res) => res.json());
};
六、簡單介紹 redux saga 與 redux observable
學習曲線很高,這兩個可能都要學超過一個月(?)其中要學 redux observable,建議先從 observable 開始,接著再進入 redux observable 比較好。