Skip to main content

[Redux] Redux Toolkit (RTK) 筆記

名稱解釋

  • 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 的 nameinitialStatereducers
  • 建立好的 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 action
  • useSelector 用來從 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'
tip

🔖 備註:action 是一個帶有 typepayload 的「物件」;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

上面雖然已經有像是 createActioncreateReducer,但如果我們能夠在一個地方同時建立 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 creatorsaction 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

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'

簡化開發者要寫 isLoadingerrordata 繁瑣的過程

  • 如果我們在 createAsyncThunk 帶入的 type 是 users/fetchByIdStatus,則它會自動產生對應的 users/fetchByIdStatus/pendingusers/fetchByIdStatus/fulfilledusers/fetchByIdStatus/rejected,並且可以透過回傳的 action 取得這些 type
  • thunk 會在執行 payload creator 之前就 dispatch pending action,之後才根據 Promise 變成對應的 fulfilled 或 rejected
  • 由於這些 action 並不是在 slice 中被定義,所以如果要在 createSlice 中監聽這些 action type,需要在 extraReducers 中透過 builder.addCase 來使用

thunkAPI 中可以取得 dispatchgetStateextraAbortSignal

// 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));