Redux middleware


Posted by Christy on 2022-01-02

本文為 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 比較好。










Related Posts

JS 引擎如何運作:理解 Execution Context 與 Variable Object

JS 引擎如何運作:理解 Execution Context 與 Variable Object

關於 Webpack 5

關於 Webpack 5

我的第一堂 - JavaScript 02 變數, 判斷式

我的第一堂 - JavaScript 02 變數, 判斷式


Comments