[Redux] Redux Toolkit (RTK) 筆記
- Redux Toolkit Quick Start @ Redux Toolkit > Tutorials > Quick Start
- Usage Guide @ Redux Toolkit > Using Redux > Toolkit > Usage Guide
名稱解釋
slice
可以透過createSlice
產生,指的是將許多的 reducer 或 actions 集合起來,通常會放在單一的檔案中,例如,counterSlice
。
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
increment: (state) => ({
value: state.value + 1,
}),
decrement: (state) => ({
value: state.value - 1,
}),
},
});
selector
是一個 function,能 夠將所需的資料從 state 中取出:
// 這個函式稱作 selector,目的是讓我們把需要的資料從 state 中取出
export const selectCount = (state: RootState) => state.counter.value;
thunk
是一個 function 讓我們能夠處理 async 的操作,見 redux-thunk @ pjchender
將 Redux Toolkit 整合進專案的步驟(Setup Redux and Redux Toolkit)
Packages
1. Install Redux Toolkit and React-Redux
npm install @reduxjs/toolkit react-redux
2. Create a Redux Store (app/store.ts
)
configureStore
可以接收reducer
後建立store
// app/store.ts
import { configureStore } from '@reduxjs/toolkit';
export const store = configureStore({
reducer: {
// ...
},
});
3. Provide the Redux Store to React (index.ts
)
- 使用
Provider
將 App 包起來 - 將剛剛透過
configureStore
建立的store
帶入<Provider />
中
// index.ts
import { store } from './app/store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
{/* Provide the Redux Store to React */}
<Provider store={store}>
<App />
</Provider>
);
4. Create a Redux State Slice (features/counter/counterSlice.ts
)
- 透過
createSlice
建立 slice,需要提供該 slice 的name
、initialState
、reducers
- 建立好的 slice 可以產生 reducer 和 action creators 供後續使用
// features/counter/counterSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
//...
},
});
// action creators
export const { increment, decrement, incrementByAmount } = counterSlice.actions;
// reducer
export default counterSlice.reducer;
5. Add Slice Reducers to the Store (app/store.ts
)
- 將 slice 產生好的 reducer 帶入 store 中
// app/store.ts
// ...
import counterReducer from '../features/counter/counterSlice';
// 透過 configureStore() 建立 Redux Store
export const store = configureStore({
reducer: {
counter: counterReducer,
},
});
6. Using Redux State and Actions in React Component(features/counter/Counter.tsx
)
useDispatch
用來 dispatch actionuseSelector
用來從 store 中 select 出想要的資料
// features/counter/Counter.tsx
import { useDispatch, useSelector } from 'react-redux';
import { RootState } from '../../app/store';
import { decrement, increment } from './counterSlice';
const Counter = () => {
const dispatch = useDispatch();
const count = useSelector((state: RootState) => state.counter.value);
const handleIncrement = () => {
dispatch(increment());
};
const handleDecrement = () => {
dispatch(decrement());
};
return {
/* ... */
};
};
APIs
configureStore
configureStore @ Redux Toolkit
在 redux 中原本就有 createStore
這個方法,而 configureStore
可以視為加強版的 createStore
,透過 configureStore()
可以簡化設定的流程、結合 slice reducers、添加和 Redux 有關的 middleware、並啟用 Redux DevTools 的擴充套件:
- 更符合 DX 的 API
- 內建 Redux DevTools Extension
- 內建 redux-thunk
// Before:
import { createStore } from 'redux';
const store = createStore(counter);
// After:
import { configureStore } from '@reduxjs/toolkit';
const store = configureStore({
reducer: counter,
});
createAction
createAction @ Redux Toolkit
createAction()
這個工具會根據 action type 回傳對應的 action creator function,這個函式本身內建 toString()
,因此可以不再額外定義 TYPE 常數。這個方法有效簡化在 redux 中需要先定義 action type 的常數,然後又在 action 中載入這個 action type 常數的冗長步驟。
透過 createAction
會:
- 自動產生一個 type 為 「
createAction
參數」的 action creator - 執行這個 action 時,會把參數的內容直接放入
action.payload
中
import { createAction } from '@reduxjs/toolkit';
// 透過 createAction 可以產生一個 action creator
const increment = createAction('counter/increment');
let action;
// 自動產生一個 type 為 counter/increment 的 action
action = increment(); // { type: 'counter/increment', payload: undefined }
// 參數內容會自動放入 action.payload 中
action = increment(3); // { type: 'counter/increment', payload: 3 }
// 複寫原本的 toString() 方法,因此可以直接取得該 action 的 type 名稱
increment.toString(); // 'counter/increment'
increment.type; // 'counter/increment'
🔖 備註:action
是一個帶有 type
和 payload
的「物件」;action creator
是一個會產生 action
的「函式」。因此,這裡的 createAction
實際上是產生一個 action creator
而不是產生一個 action
。
原本使用 redux 的寫法:
const INCREMENT = 'counter/increment';
// 這是一個 action creator
function increment(amount) {
return {
type: INCREMENT,
payload: amount,
};
}
// 這是一個 action
const action = increment(3); // { type: 'counter/increment', payload: 3 }
在 builder 中使用 action
- createAction 改寫了內部的
toString()
方法,因此可以直接把它帶入builder.addCase()
const addTodo = createAction('ADD_TODO');
addTodo({ text: 'Buy milk' });
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
addTodo.toString(); // "ADD_TODO"
addTodo.type; // "ADD_TODO"
const reducer = createReducer({}, (builder) => {
// actionCreator.toString() 會自動被呼叫,使用 TS 時能正確推導型別
builder.addCase(actionCreator, (state, action) => {});
// 也可以自己使用 actionCreator.type,但這樣就不能正確推導型別了
builder.addCase(actionCreator.type, (state, action) => {});
});
createReducer
createReducer @ Redux Toolkit
createReducer()
這個工具提供你一張 action type 和 reducer 的對應表,而不用使用 switch
語法,此外它自動使用 immer
library 這個工具,讓你可以使用「immutable」的方式來變更資料狀態,例如,state.todos[3].completed = true
。
// 使用 createReducer
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');
// 使用 ES6 中物件的 computed property 語法
// 原本是寫 [increment.type] 但因為 increment 內建 toString() 方法,所以可以寫成 [increment]
const counter = createReducer(0, {
[increment]: (state, action) => state + 1,
[decrement]: (state, action) => state - 1,
});
const todosReducer = createReducer([], (builder) => {
builder
.addCase('ADD_TODO', (state, action) => {
// "mutate" the array by calling push()
state.push(action.payload);
})
.addCase('TOGGLE_TODO', (state, action) => {
const todo = state[action.payload.index];
// "mutate" the object by overwriting a field
todo.completed = !todo.completed;
})
.addCase('REMOVE_TODO', (state, action) => {
// Can still return an immutably-updated value if we want to
return state.filter((todo, i) => i !== action.payload.index);
});
});
原本 Redux 中的寫法:
// 這是一個 reducer
const counter = (state = 0, action) => {
if (typeof state === 'undefined') {
return 0;
}
switch (action.type) {
case increment.type:
return state + 1;
case decrement.type:
return state - 1;
default:
return state;
}
};
createSlice
createSlice @ Redux Toolkit
上面雖然已經有像是 createAction
和 createReducer
,但如果我們能夠在一個地方同時建立 action creator、reducers 的話,管理起來應該會變得更為方便, createSlice()
於是產生。
在 createSlice()
這個函式中可以帶入 reducer function, slice name 和 initial state,將自動產生對應的 slice reducer,並包含對應的 action creators 和 action types 在內。
以剛剛的例子來說,使用 createSlice
前:
// 原本使用 createAction 和 createReducer
const increment = createAction('INCREMENT');
const decrement = createAction('DECREMENT');
const counter = createReducer(0, {
[increment]: (state) => state + 1,
[decrement]: (state) => state - 1,
});
const store = configureStore({
reducer: counter,
});
document.getElementById('increment').addEventListener('click', () => {
store.dispatch(increment());
});
使用 createSlice
後,透過 createSlice
將自動產生 action creators 和 action types:
// 使用 createSlice 後
const counterSlice = createSlice({
name: 'counter',
initialState: 0,
reducers: {
increment: (state, action) => state + 1,
decrement: (state, action) => state - 1,
incrementByAmount: (state, action) => state + action.payload,
},
});
console.log(counterSlice);
/*
{
name: 'counter',
actions: {
// action creators...
increment,
decrement,
incrementByAmount,
}
reducer
}
*/
// 從 slice 中拿出 action creator
const { increment, decrement, incrementByAmount } = counterSlice.actions;
console.log(incrementByAmount(3)); // { type: 'counter/incrementByAmount', payload: 3 }
const store = configureStore({
reducer: counterSlice.reducer,
});
document.getElementById('increment').addEventListener('click', () => {
store.dispatch(counterSlice.actions.incrementByAmount(3));
});
在上述 createSlice
產生的 counterSlice
物件中有這些可用的屬性和方法:
// slice 的名稱
counterSlice.name; // "counter"
// actions type
counterSlice.actions.increment.type; // "counter/increment",一樣有內建 toString 方法
counterSlice.actions.decrement.type; // "counter/increment",一樣有內建 toString 方法
// dispatch action
counterSlice.actions.increment(); // counterSlice.actions.increment 是 action creator
counterSlice.actions.decrement();
// reducer
counterSlice.reducer;
createSelector
createSelector @ Redux Toolkit
createSelector
這個工具來自 Reselect 這個套件。
createAsyncThunk
- createAsyncThunk @ Redux Toolkit > API Reference
- Using createAsyncThunk @ Redux > Tutorials > Redux Fundamentals
const fetchUserById = createAsyncThunk(
// type
'users/fetchByIdStatus',
// payloadCreator(arg, thunkAPI)
async (userId, thunkAPI) => {
const { dispatch, getState, extra, requestId, signal, rejectWithValue, fulfillWithValue } = thunkAPI;
const response = await userAPI.fetchById(userId)
return response.data
}
)
fetchUserById.pending(); // 'users/fetchByIdStatus/pending'
fetchUserById.fulfilled(); // 'users/fetchByIdStatus/fulfilled'
fetchUserById.rejected(); // 'users/fetchByIdStatus/rejected'
簡化開發者要寫 isLoading
、error
和 data
繁瑣的過程
- 如果我們在
createAsyncThunk
帶入的 type 是users/fetchByIdStatus
,則它會自動產生對應的users/fetchByIdStatus/pending
、users/fetchByIdStatus/fulfilled
、users/fetchByIdStatus/rejected
,並且可以透過回傳的 action 取得這些 type - thunk 會在執行 payload creator 之前就 dispatch pending action,之後才根據 Promise 變成對應的 fulfilled 或 rejected
- 由於這些 action 並不是在 slice 中被定義,所以如果要在 createSlice 中監聽這些 action type,需要在
extraReducers
中透過builder.addCase
來使用
在 thunkAPI
中可以取得 dispatch
、getState
、extra
、 AbortSignal
等
// First, create the thunk
// 透過 createAsyncThunk 會產生對應的 action type
// `users/fetchByIdStatus/pending`、`users/fetchByIdStatus/fulfilled`、`users/fetchByIdStatus/rejected`
// 並可以用 fetchUserById.pending、fetchUserById.fulfilled、fetchUserById.rejected 取得
const fetchUserById = createAsyncThunk('users/fetchByIdStatus', async (userId, thunkAPI) => {
const response = await userAPI.fetchById(userId);
return response.data;
});
// Then, handle actions in your reducers:
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {
// standard reducer logic, with auto-generated action types per reducer
},
extraReducers: (builder) => {
// Add reducers for additional action types here, and handle loading state as needed
builder.addCase(fetchUserById.fulfilled, (state, action) => {
// Add user to the state array
state.entities.push(action.payload);
});
},
});
// Later, dispatch the thunk as needed in the app
dispatch(fetchUserById(123));