Redux Toolkit的configureStore API不需要任何額外的typing。但是,你需要提取RootState類型和Dispatch類型,以便可以根據需要進行引用。
從store本身推斷這些類型意味著當你添加更多state slices或修改middleware設置時,它們會正確更新。
// components/redux/store/store.tsx
import { configureStore } from '@reduxjs/toolkit'
import counterReducer from '../slice/counterSlice'
import userReducer from '../slice/userSlice'
export const store = configureStore({
reducer: {
// 我寫了兩個reducer, 一個是counterReducer, 一個在使用上我還改名為user
counterReducer,
user: userReducer
}
})
// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch
configureStore() 會自動組合你的 reducer,並建立 Redux store。它會回傳一個 Redux store 物件,你可以使用它來呼叫 getState() 取得目前的 state,或是 dispatch(action) 來更新 state。
而這邊新增的兩個部分則是RootState和AppDispatch,這兩個型別是用來幫助我們在使用Redux時,可以更方便的使用型別推論,而不用每次都要自己去宣告型別。
而這邊的RootState則是用來取得目前的state,而AppDispatch則是用來dispatch action
白話:這兩步驟(RootState和AppDispatch)是為了之後的型別推論 保證型別的正確性
由於Redux store的dispatch和selector函數都需要使用RootState和AppDispatch類型,因此我們可以為它們定義一個hook,以便可以在需要的地方使用它們。
在每個組件中導入RootState和AppDispatch類型是可行的,但最好在你的應用程序中創建具有類型定義的useDispatch和useSelector hooks。這有幾個原因:
對於useSelector,它可以省去每次輸入(state: RootState)的麻煩。
使用默認Dispatch類型的useDispatch並不知道thunk。為了正確地分發thunk,需要在store中使用特定的自定義AppDispatch類型,該類型包括thunk middleware類型,並使用它來替換useDispatch。添加預先定義的useDispatch hook可以避免忘記在需要時導入AppDispatch。
由於這些都是實際變量而不是類型,因此將它們定義在單獨的文件中(例如app/hooks.ts),而不是儲存設置文件中非常重要。
這使你可以將它們導入到任何需要使用hooks的組件文件中,並避免潛在的循環引入依賴問題。
白話:自定義hook 這樣就可以在其他地方使用useAppDispatch和useAppSelector 同時使用前面的型別推論
可以確保我們使用useAppDispatch和useAppSelector的時候都只會使用到我們在store裡面定義的型別
// components/redux/hook/hook.tsx
import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'
import type { RootState, AppDispatch } from '../store/store'
// Use throughout your app instead of plain `useDispatch` and `useSelector`
export const useAppDispatch: () => AppDispatch = useDispatch
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
現在,我們已經定義了RootState和AppDispatch類型,接下來我們要定義State、Action與reducer(當然做法會與舊版的不同)
首先是State
我們宣告了兩個State,一個是計算器的State,一個是使用者的State,並且我們也都給予了初始值
// components/redux/state/stateType.tsx
// Define a type for the slice state
export interface CounterState {
value: number
}
// Define the initial state using that type
export const initialState: CounterState = {
value: 0,
}
/////////////////////////////////////// 第二個State
export interface UserState {
username: string
age: number
email: string
}
export const userInitialState: UserState = {
username: '',
age: 0,
email: ''
}
接著是Slice(更改Reducer和Action的寫法)
那因為我在前面 說要使用兩個reducer (兩個slice的意思) 所以我們這邊會寫兩個slice.tsx
在 createSlice() 這個函式中可以帶入 reducer function、slice name 和 initial state,將自動產生對應的 slice reducer,並包含對應的 action creators 和 action types 在內。 在使用 createSlice 或 createReducer 撰寫 reducer 的時候可以不用再用 switch case 語法,它們的語法底層加入了 immer,因此可以使用會有 side effect 的寫法去變更 state(直接修改 state),它背後會再幫你轉成 「immutable」的方式。
補充:它們的語法底層加入了 immer,因此可以使用會有 side effect 的寫法去變更 state(直接修改 state),它背後會再幫你轉成 「immutable」。
Redux Toolkit 裡的 createSlice 和 createReducer 函式底層採用了 immer 這個函式庫去實現直接修改 state 的方式,並且背後會幫你轉成 immutable 的方式。 因此開發者在使用 createSlice 和 createReducer 撰寫 reducer 的時候可以使用有 side effect 的方式去變更 state。 例如:可以直接將 state 裡的物件、陣列等「直接修改」,而不需要像以前一樣必須「複製整個物件/陣列」然後再修改某一個值。
immer 是一個 JavaScript 函式庫,它可以讓你使用基於物件的程式碼來修改深層次的 state 物件,而不需要使用傳統的複製和粘貼方式。 immer 使用起來比傳統 Immutable Library 更加簡潔,並且可以讓你的程式碼更易讀、更容易維護。
補充:必須「複製整個物件/陣列」然後再修改某一個值意思
在傳統的 JavaScript 上,如果你想要修改一個物件裡面的某個值,必須先使用 Object.assign() 或是 spread operator (...) 這些方法複製一份全新的物件,然後再修改其中的屬性。例如:
// 這是一個簡單的物件
const myObj = { a: 1, b: 2 };
const newObj = Object.assign({}, myObj, {b: 3});
// 或是 const newObj = {...myObj, b: 3};
console.log(myObj); // {a: 1, b: 2}
console.log(newObj); // {a: 1, b: 3}
這樣做的問題在於,當物件變得很大時,複製整個物件會佔用大量的記憶體,並且容易導致效能問題。
在使用 immer 時,你可以直接修改物件裡面的值,而不需要複製整個物件,例如:
import produce from 'immer';
const myObj = { a: 1, b: 2 };
const newObject = produce(myObj, draft => {
draft.b = 3;
});
console.log(myObj); // { a: 1, b: 2 }
console.log(newObject); // { a: 1, b: 3 }
在上面的範例中,使用 produce() 函数從傳入的 state 物件創建一份 immutable 的複本(也就是 draft 物件),然後你可以直接修改 draft 物件裡面的屬性值,這樣就不需要額外地創建和更新新的物件了。
回歸到我們的主題
第一個slice:counterSlice.tsx
// component/redux/slice/counterSlice.tsx
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { initialState } from "../state/stateType"
import { RootState } from "../store/store"
export const counterSlice = createSlice({
name: 'counter',
// `createSlice` will infer the state type from the `initialState` argument
initialState,
reducers: {
// slice 會自動產生 action creators 和 action types
increment: (state) => {
state.value += 1
},
decrement: (state) => {
state.value -= 1
},
// Use the PayloadAction type to declare the contents of `action.payload`
incrementByAmount: (state, action: PayloadAction<number>) => {
state.value += action.payload
},
},
})
export const { increment, decrement, incrementByAmount } = counterSlice.actions
// Other code such as selectors can use the imported `RootState` type
export const selectCount = (state: RootState) => state.counterReducer.value
export default counterSlice.reducer
第二個slice:userSlice.tsx
// component/redux/slice/userSlice.tsx
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { userInitialState } from "../state/stateType"
import { RootState } from "../store/store"
export const userSlice = createSlice({
name: 'user',
initialState: userInitialState,
reducers: {
setUsername: (state, action: PayloadAction<string>) => {
state.username = action.payload
},
setUserAge: (state, action: PayloadAction<number>) => {
state.age = action.payload
},
setUserEmail: (state, action: PayloadAction<string>) => {
state.email = action.payload
}
}
})
export const { setUsername, setUserAge, setUserEmail } = userSlice.actions
export const selectUsername = (state: RootState) => state.user.username
export const selectAge = (state: RootState) => state.user.age
export const selectEmail = (state: RootState) => state.user.email
export default userSlice.reducer
這樣就完成了,就可以透過useAppSelector來取得State的值了以及透過useAppDispatch來分發Action了
你可以這樣使用,這邊就只顯示加與減的code,詳細的請點連結去看~
function Counter() {
// const count = useAppSelector((state) => state.counterReducer.value)
// 因為我們前面有建立好了selectCount,所以我們可以這樣使用
const count = useAppSelector(selectCount)
const dispatch = useAppDispatch()
return (
<div>
<div>{count}</div>
<button onClick={() => dispatch(increment())}>+</button>
<button onClick={() => dispatch(decrement())}>-</button>
</div>
)}
上面的範例與程式碼連結:
體驗我們寫好的redux完整程式碼這邊我們使用redux-thunk來處理非同步
首先是安裝,如果你有安裝toolkit就不用再安裝了
npm install redux-thunk
接著是修改store,但現在基本上預設都有thunk了,所以根本不用修改
// component/redux/store/store.tsx
import { configureStore } from "@reduxjs/toolkit";
import thunk from "redux-thunk";
import { reducer } from "../reducer/reducer";
export const store = configureStore({
reducer: {
firstReducer: reducer
},
// middleware: [thunk]
// 預設就是true,但是如果你要自己設定的話,可以這樣寫(根本沒影響 除非你要關閉)
// 關閉就會
// Error: Actions must be plain objects. Use custom middleware for async actions.
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
thunk: true
})
})
接著是寫一個非同步的action,我們會使用兩個API元件,一個是createAsyncThunk,一個是createSlice(然後加上extraReducers)
createAsyncThunk可以協助我們管理異步的狀態,createAsyncThunk 接收兩個參數:typePrefix 和 payloadCreator。
typePrefix 是字串,在你指定的名稱前都會加上字首(通常是動詞),以便創建有意義的 Redux action 類型。(白話的說就是取一個方便你閱讀的名字)
而 payloadCreator 是一個函式,負責處理異步邏輯並返回一個 Promise 物件,當該 Promise 物件解析完成時,將其值傳遞給 Redux store。
createAsyncThunk 函式會自動創建相應的 Redux reducer,以處理異步 action。這些 reducer 會將異步 action 的狀態儲存在 Redux store 中,並且會自動創建三個 action creator:pending、fulfilled 和 rejected。
接著是createSlice,這邊我們要注意的是,我們要在createSlice的extraReducers裡面去寫pending、fulfilled、rejected,用來接收createAsyncThunk的回傳值
extraReducers意思是允許 createSlice 去針對非此 createSlice 所創建的 actions 做出回應。extraReducers 就是用來參照 「外部」的 actions,它們不會出現在 sliceObject.actions 中。
extraReducers是用來接收非此createSlice所創建的actions,像是createAsyncThunk的回傳值。
寫法是綁住builder,而builder又有三個方法分別是addCase、addMatcher、addDefaultCase。
!!請注意這三個method有順序限制,一定要是addCase、addMatcher、addDefaultCase。
簡短的介紹一下
addCase:添加一個可變 reducer,用於匹配指定 action type。 最常用的方法,用於匹配指定的 action type,並對 state 進行更新。
const userSlice = createSlice({
name: 'users',
initialState: { users: [], userNames: [] },
reducers: {},
extraReducers: builder => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.users = action.payload
})
}
})
addMatcher:添加一個自定義 matcher 函數,用於匹配特定條件下的 action。 addMatcher()可以用於針對特定條件下的action做出回應,例如當一個 action 的 payload 符合某些特定的條件時才進行相應的狀態更新。 這意味著,相比使用addCase(),使用addMatcher()能夠更細粒度地控制 reducer 如何響應 action,從而使得代碼更加具有可讀性和可维護性。 舉個例子,假設我們有一個Redux store來保存用戶列表,當新增用戶時,不僅要更新state中的users列表,還要在另一個state中的userNames列表中增加此用戶的名字。 此時就可以利用addMatcher()函式根據action的payload來判斷是否需要對應處理:實作範例連結
const userSlice = createSlice({
name: 'users',
initialState: { users: [], userNames: [] },
reducers: {},
extraReducers: builder => {
builder.addMatcher(
action => action.type === 'users/addUser' && action.payload.user.age > 18, // 條件:新增用戶的年齡大於18歲
(state, action) => {
state.users.push(action.payload.user); // 在users列表中添加新用戶
state.userNames.push(action.payload.user.name); // 在userNames列表中添加新用户的名字
}
)
}
})
addDefaultCase:添加一個默認的回傳語句,用於當創建的 reducer 未找到對應的 action type 時使用。 addDefaultCase()可以用於當創建的 reducer 未找到對應的 action type 時使用,這個方法的參數是一個函數,該函數的參數是state和action,返回值是state。 這個方法的用法和addCase()類似,但是它是用於處理所有未匹配到的action,因此它的優先級最低。
const userSlice = createSlice({
name: 'users',
initialState: { users: [], userNames: [] },
reducers: {},
extraReducers: builder => {
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.users = action.payload
})
builder.addMatcher(
action => action.type === 'users/addUser' && action.payload.user.age > 18, // 條件:新增用戶的年齡大於18歲
(state, action) => {
state.users.push(action.payload.user); // 在users列表中添加新用戶
state.userNames.push(action.payload.user.name); // 在userNames列表中添加新用户的名字
}
)
builder.addDefaultCase((state, action) => {
// 未匹配到的action
})
}
})
// component/redux/slice/asyncSlice.tsx
import { createAsyncThunk, createSlice, Dispatch, PayloadAction } from "@reduxjs/toolkit"
import { asyncInitialState } from "../state/stateType"
import { RootState } from "../store/store"
export const fetchFirstData = createAsyncThunk(
'first/fetchData',
async () => {
const response = await fetch('https://jsonplaceholder.typicode.com/users')
const data = await response.json()
return data
}
)
const asyncSlice = createSlice({
name: 'asyncData',
initialState: asyncInitialState,
reducers: {
},
// extraReducers是用來接收非此createSlice所創建的actions,像是createAsyncThunk的回傳值
// 寫法是綁住builder,而builder又有三個方法分別是addCase、addMatcher、addDefaultCase
// 請注意這三個method有順序限制,一定要是addCase、addMatcher、addDefaultCase
// 這邊使用addCase就可以了,因為我們只有一個action,也沒有其他的特殊需求
extraReducers: (builder) => {
builder
.addCase(fetchFirstData.pending, (state) => {
state.isLoaded = true
})
// fulfilled是當action成功執行時的回傳值 就去更新state
.addCase(fetchFirstData.fulfilled, (state, action) => {
state.isLoaded = false
state.isComplete = true
state.AsyncStateList = action.payload
})
.addCase(fetchFirstData.rejected, (state) => {
state.isLoaded = false
state.isComplete = false
})
}
})
export const selectAsync = (state: RootState) => state.async
export default asyncSlice.reducer
當然你必須修改State,要符合你的需求,完整版都在下方連結中。
體驗我們寫好的redux完整程式碼我們寫好的slice,可以把裡面的reducer拆成多個function,然後使用,這樣的好處是不會讓createSlice變得太長,也可以讓你的程式碼更好維護。
完整程式碼