昨天已經看了一些RTK的基本用法,今天一樣是使用RTK繼續完成我們的購物車。雖然昨天已經完成了大部分的功能,可以把商品加入到購物車了,但是我們的商品清單目前是暫時先寫死的狀態,也就是沒有透過api取得當前最新的商品列表。前幾天有提到使用Redux時,通常會透過Middleware處理非同步的操作,今天就從Middleware是什麼開始認識,並在最後完成我們購物車串接api的部分吧!
在Redux中,Middleware會用來處理dispatch一個action後,到更新store state觸發畫面重新渲染之間需要額外做的一些動作,這些動作包含處理打api相關的操作、記錄log等。簡單來說,就是讓dispatch action的動作,不只是很單純地送出一個action來更新store的state,還在中間做了其他的事情
。
雖然不使用Middleware一樣也可以操作非同步或記錄log的動作,但是如果不使用Middleware,就必須自己去管理這些動作的狀態,例如串接api時,發送request後的不同狀態,像是pending、fullfilled、rejected。
這裡先稍微看一下,沒使用Middleware的時候,要怎麼處理非同步的動作。
首先會需要定義打API後,會有的pending、fullfilled、rejected的狀態,以及與其對應的action,如果是用一般的Redux的寫法,需要透過action creator來產生處理這三個狀態的action物件。
例如以下這樣的寫法
const fetchProductsDataRequest = () => ({
type: 'FETCH_PRODUCTS_DATA_REQUEST',
});
const fetchProductsDataSuccess = (data) => ({
type: 'FETCH_PRODUCTS_DATA_SUCCESS',
payload: data,
});
const fetchProductsDataFailure = (error) => ({
type: 'FETCH_PRODUCTS_DATA_FAILURE',
error: error,
});
但是因為今天我們是使用RTK,createSlice會幫我們自動處理action creator,所以可以省略這個步驟。不過由於會需要把fetch到的資料存在store裡面,所以還是會需要先使用createSlice來定義store。
import { createSlice } from "@reduxjs/toolkit";
const initialState = {
products: null,
loading: false,
error: null,
};
export const productsDataSlice = createSlice({
name: 'productData',
initialState,
reducers: {
// 定義接API時,處理對應狀態的action
fetchDataRequest: (state) => (
{
...state,
loading: true,
error: null,
}
),
fetchDataSuccess: (state, action) => (
{
...state,
...action.payload,
products: action.payload,
loading: false,
error: null,
}
),
fetchDataError: (state, action) => (
{
...state,
products: null,
loading: false,
error: action.payload,
}
),
}
});
export const { fetchDataRequest, fetchDataSuccess, fetchDataError } = productsDataSlice.actions;
export default productsDataSlice.reducer;
action的定義好後,再透過定義的action,來處理打API時,需依照狀態改變來進行的動作。
import { useEffect } from "react";
import ProductListItem from "./ProductListItem";
import { useDispatch , useSelector } from "react-redux";
import { addProductItem } from "../slices/shoppingCart";
import { fetchDataRequest, fetchDataSuccess, fetchDataError } from "../slices/productsData";
export default function ProductList() {
const dispatch = useDispatch();
const { products } = useSelector(state => state.productsDataReducer);
const addProductToCart = (payload) => {
dispatch(addProductItem(payload));
};
useEffect(() => {
const fetchProductsData = () => {
// dispatch 開始發送api request
dispatch(fetchDataRequest());
fetch('https://api.escuelajs.co/api/v1/products')
.then((response) => {
return response.json();
})
.then((data) => {
// dispatch 成功的 action
dispatch(fetchDataSuccess(data));
})
.catch((error) => {
// dispatch 失敗的 action
dispatch(fetchDataError(error.message));
});
};
fetchProductsData()
}, [dispatch]);
return (
<div className="product-list">
<h2>Product List</h2>
<div className="product-item-container">
{
products && products.map((product) => (
<ProductListItem product={product} key={product.id} addProductToCart={addProductToCart} />
))
}
</div>
</div>
)
}
從這個例子可以發現即使沒有使用Middleware,一樣也可以處理非同步的操作,但是非同步操作和錯誤處理都包會被含在React元件中,並且需要手動dispatch action。這樣的寫法也就會使得元件的程式碼變得很長且難以理解,尤其是如果有多個非同步操作,那程式碼就會變得更複雜。
目前已經看了沒有使用任何Middleware的寫法了,接下來再來看看如果使用React Thunk這個Middleware的話,寫法上會有什麼樣的調整。在有使用Middleware的情境下,一樣會需要使用action creator來定義action。如果是使用一般Redux搭配redux thunk,會需要先以下述的寫法來定義action。
const fetchDataRequest = () => ({
type: 'FETCH_DATA_PENDING',
});
const fetchDataSuccess = () => ({
type: 'FETCH_DATA_FULFILLED',
});
const fetchDataFailure = (error) => ({
type: 'FETCH_DATA_ERROR',
error: error,
});
今天我們是使用RTK,則可以透過createAsyncThunk
來簡化一些比較繁瑣的步驟,雖然createAsyncThunk並不是middleware本身,但是也是在建立在middleware的基礎之上。另外,由於RTK預設的MiddleWare是thunk,所以在使用createAsyncThunk,不需要額外安裝thunk。
使用createAsyncThunk有兩個一定要帶入的參數,分別actionTypePrefix
和asyncThunkFunction
,另一個參數則可以依照實際情況再選擇要不要帶上。
actionTypePrefix會讓createAsyncThunk產生的三個狀態的action type都被帶上透過這個參數帶上的前綴,如果是fetchProductsData,產生的action type也就會是fetchProductsData/pending、fetchProductsData/fulfilled 和fetchProductsData/rejected;asyncThunkFunction則是處理非同步操作的函式。
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
// 使用createAsyncThunk來處理api的操作
export const fetchProductsData = createAsyncThunk('fetchProductsData', async () => {
try {
const response = await fetch('https://api.escuelajs.co/api/v1/products');
const data = await response.json();
return data;
} catch (error) {
throw error;
}
});
const initialState = {
products: null,
isLoading: true,
error: null,
};
export const productsDataSlice = createSlice({
name: 'productsData',
initialState,
reducers: {},
extraReducers: (builder) => {
// 增加關於fetch data有關三個狀態
builder.addCase(fetchProductsData.pending, (state) => {
state.isLoading = true; // 正在fetch資料
})
.addCase(fetchProductsData.fulfilled, (state, action) => {
// fetch成功
state.products = action.payload;
state.isLoading = false;
})
.addCase(fetchProductsData.rejected, (state, action) => {
state.isLoading = false; // fetch失敗
state.error = action.error.message;
});
},
});
export default productsDataSlice.reducer;
最後記得還是要把這個reducer帶到createStore裡面,這樣才可以取得透過API拉取並儲存於store內的商品資料。
import { configureStore } from "@reduxjs/toolkit";
import shoppingCartReducer from './slices/shoppingCart';
import productsDataReducer from "./slices/productsData";
const store = configureStore({
reducer: {
shoppingCartReducer,
// 把productsDataReducer也加上
productsDataReducer,
},
});
export default store;
前置作業都完成後,就可以在需要打API的元件中,補上打API拉取資料的動作,以及透過useSelector取得store內state的動作。
import { fetchProductsData } from '../slices/productsData';
const { products, isLoading } = useSelector(state => state.productsDataReducer);
useEffect(() => {
dispatch(fetchProductsData());
}, [dispatch]);
搭配Redux Thunk這個Middleware時,雖然在元件中使用的時候,只需要 dispatch一個action creator,而不需要在每個階段都手動dispatch不同的 action,但在編寫action creator時,仍然需要根據非同步操作中的不同狀態,來定義不同的action。
也就是說,即使用了Middleware仍然需要在action creator中定義不同的action type,以區分不同的非同步操作
,例如開始、成功、失敗等。這樣才可以確保在不同情況下,Redux Thunk都能夠正確處理相對應的action,並通知 reducer進行狀態更新。所以使用Middleware雖然可以將處理非同步操作的程式碼集中管理,但仍然無法用更簡潔的方式處理非同步的操作或更新state之前的動作。如果想解決步驟繁瑣的問題的話,官方推薦搭配RTK一起使用的RTK Query就是一個很合適的用法。
RTK Query是React Toolkit提供來fetch及cache資料的工具,主要目的是簡化fetch資料時,需要手寫相關邏輯的工。當使用RTK Query時,它會自動產生store、action和reducer,也會自動處理撰寫串接API時,需要處理pedding、fulfilled、error的三個狀態,也就可以省略掉需要使用action creator撰寫串接API時,需要手動定義及處理這三個狀態的這個步驟。透過這樣的做法,就能讓整體的程式碼變得更簡潔。
前面已經看了有使用Midleware和沒有使用Middleware的情境,並且認識RTK Query是什麼了,接下來直接透過使用RTK Query的實作,實際感受一下使用RTK Query和使用Middleware有什麼差異吧!
首先透過createApi
建立一個productsDataApi的檔案來定義API,並且用fetchBaseQuery
來建立一個使用指定URL的fetch函式。
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const productsDataApi = createApi({
reducerPath: 'productsDataApi',
// 設定指定base URL的fetch函式
baseQuery: fetchBaseQuery({ baseUrl: 'https://api.escuelajs.co/api/v1' }),
endpoints: builder => ({
// 定義一些api的操作,例如get, post等
getProductsData: builder.query({
query: () => `/products`
})
})
});
// 這裡export到外面的hook名稱,會依照上面endpoint取的名字加上use和query
export const { useGetProductsDataQuery } = productsDataApi;
使用createApi定義的API,會回傳以下的這些內容,這裡主要會使用到的是reducer。
定義好的api之後,一樣也需要把它設定到store上,才能夠正常使用。主要會有兩個步驟,一個把reducer設定上去
,另一個則是把productsDataApi的Middleware和預設的Middleware合併在一起
。
import { configureStore } from "@reduxjs/toolkit";
import shoppingCartReducer from './slices/shoppingCart';
import { productsDataApi } from "./apis/productsDataApi";
const store = configureStore({
reducer: {
shoppingCartReducer,
// 把reducer加上
[productsDataApi.reducerPath]: productsDataApi.reducer,
},
// 取得預設的middleware,並把createApi產生的middleware和預設的middleware合併成一個新的middleware
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(productsDataApi.middleware)
});
export default store;
在需要使用的地方把useGetProductsDataQuery這個hook import進去。
import { useGetProductsDataQuery } from "../apis/productsDataApi";
export default function ProductList() {
// 透過useGetProductsDataQuery可以快速取得data, error, isLoading
const {data, error, isLoading} = useGetProductsDataQuery();
return (
<div className="product-list">
<h2>Product List</h2>
<div className="product-item-container">
// 這樣就可以輕鬆地把data和isLoading拿來使用
{
isLoading ? <p>Loading...</p> : data.map((product) => (
<ProductListItem product={product} key={product.id} addProductToCart={addProductToCart} />
))
}
</div>
</div>
)
}
以上幾行程式碼,就可以輕輕鬆鬆地處理串接API的動作,而且不需要額外定義有關products的store,也不用創建action處理API相關的狀態,讓整體的程式碼更簡潔好維護。如果需要在處理API這樣非同步的操作時,做更多的處理時,也可以透過客製化hook的方式進行。
最後完成的repo在這裡
今天看了在redux中要怎麼處理非同步的操作,這之中嘗試了沒有透過middleware處理的方法,也嘗試了有middleware處理的方法,還近一步使用RTK Query實作,雖然三種方法都可以達到目的,但是也可以很明顯地感受出三種不同做法上,程式碼複雜與否的差異。從這次的實務過程,也能更熟悉redux在操作資料上的整體概念,也就是「透過action下去決定要做什麼操作」,不論是同步的動作,還是非同步的動作,都是以這個相同的概念下去執行。
今天到這裡已經把redux的部分告一個段落,明天會來接著看完成一個Vue或React專案也很重要的一個部分,也就是Route路由的部分。