Redux 使用流程
回憶一下 Redux 的使用流程:
- 創建 reducer 整合函數
- 通過 reducer 物件創建 store
- 對 store 中的 state 進行訂閱
- 通過 dispatch 派發 state 的操作指令
// 1. 創建 reducer 整合函數
function reducer(state = { count: 1, name: 'Tom' }, action) {
// state 表示當前 state 可以根據這個 state 生成新的 state
// action 是一個 js 物件,它裡面會保存操作的信息
switch (action.type) {
case 'ADD':
return { ...state, count: state.count + 1 };
case 'SUB':
return { ...state, count: state.count - 1 };
default:
return state
}
}
// 2. 通過 reducer 物件創建 store
const store = Redux.createStore(reducer)
// 3. 對 store 中的 state 進行訂閱
store.subscribe(() => {
// 當 state 發生變化時輸出 state
console.log(store.getState())
});
// 4. 通過 dispatch 派發 state 的操作指令
store.dispatch({ type: 'SUB' })
store.dispatch({ type: 'ADD' })
若按照上方的寫法繼續寫下去,可能會出現以下問題:
- 如果 state 過於複雜,將會變得很難維護。
- 解決方式:可以通過對 state 分組來解決這個問題,創建多個 reducer,然後將其合併為一個。
- state 每次操作時,都需要對其進行複製,然後再去修改。
- case 後面的常數維護起來比較麻煩。
Redux Toolkit (RTK)
Redux 提供了一種使用 Redux 的方式——Redux Toolkit,簡稱 RTK。RTK 可以幫助我們處理使用 Redux 過程中的重複性工作,簡化 Redux 中的各種操作。
無論你是開發新專案還是想簡化現有應用,都建議使用 Redux Toolkit ,可以優化你的程式碼,使其更便於維護。
安裝 RTK
RTK 可以完全替換 Redux。
npm install @reduxjs/toolkit react-redux
// or
yarn add @reduxjs/toolkit react-redux
創建 Slice
createSlice()
用於同步建立 reducer 和 action,參數是一個物件,物件中有幾個常用的參數:
- name: slice 的名稱
- initialState: slice 的初始狀態
- reducers: 指定 slice 的操作,底層有用到
createAction()
和createReducer()
,因此會自動幫忙建立 actions
RTK 的
createSlice()
和createReducer()
自動使用 immer 套件,所以可以直接用 immutable 的方式更新 state
// src/store/index.js
import { createSlice } from '@reduxjs/toolkit'
// createSlice 創建 slice
// 它需要一個物件作為參數
const studentSlice = createSlice({
name: 'student', // 用來自動生成 action 中的 type
initialState: { // 當前切片的 state 的初始值
name: 'Tom',
age: 20,
gender: 'Male'
},
reducers: { // 指定 state 的操作,直接在物件中添加方法
setName(state, action) {
// 可以通過不同的方法來指定對 state 的不同操作
// state 是一個代理物件,可以直接修改
state.name = 'Peter'
},
setAge(state, action) {
state.age = 10
}
}
})
action
把剛剛創建好的 studentSlice
log 出來,slice 物件的結構如下:
前面有提到 createSlice 底層有用到 createAction
,createAction
回傳的是 actionCreator
函數,調用函數才後會生成 action
物件。
而 action 物件中包含 payload
和 type
:
- type:
slice名稱/reducer函數名稱
- payload: 函數的參數
const { setName, setAge } = studentSlice.actions
const nameAction = setName('Olivia')
const ageAction = setAge(30)
console.log(nameAction, ageAction)
通常來說 actions 會需要在其他檔案中被調用,所以直接在最後 export 出去:
// src/store/index.js
import { createSlice } from '@reduxjs/toolkit'
const studentSlice = createSlice({
name: 'student',
initialState: {
name: 'Tom',
age: 20,
gender: 'Male'
},
reducers: {
setName(state, action) {
// action: { type: 'student/setName', payload: '傳入的參數' }
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
export const { setName, setAge } = studentSlice.actions
小結:
createSlice
=createReducer
+createAction
- action 中存儲的是 slice 自動生成的 actionCreator 函數,調用函數後才會創建 action 物件。
- action 物件的結構為
{ type: slice名稱/reducer函數名稱, payload: 函數的參數}
。
創建 Store
使用 configureStore
接收 Reducers 後創建 Store:
// src/store/index.js
import { createSlice, configureStore } from '@reduxjs/toolkit'
...
const store = configureStore({
// reducer 可以有很多個,所以要寫成物件的形式
reducer: {
student: studentSlice.reducer
}
})
export default store
要在應用中操作 store 需要先在 main.jsx
中引入 Provider,並傳入剛剛創建好的 store
:
// main.jsx
import React from 'react'
import ReactDOM from 'react-dom/client'
import { Provider } from 'react-redux'
import App from './App.jsx'
import store from './store'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<App />
</Provider>
)
獲取 store 中的 state
若要在組件中讀取 store 中的 state 可以使用 useSelector
hook:
// App.jsx
import { useSelector } from 'react-redux'
const App = () => {
const store = useSelector(state => state)
console.log(store)
return <></>
}
export default App
store 中給了幾個 reducer 就會拿到幾個 state:
// src/store/index.js
...
const store = configureStore({
reducer: {
student: studentSlice.reducer,
teacher: teacherSlice.reducer
}
})
假設只需要獲取 student 的 state 就使用 useSelector(state => state.student)
// App.jsx
import { useSelector } from 'react-redux'
const App = () => {
const student = useSelector(state => state.student)
console.log(store)
return <></>
}
export default App
修改 store 中 state 的值
以下方程式碼為例,student 中有兩個方法 setName
, setAge
可以修改 name
, age
的值:
// src/store/index.js
const studentSlice = createSlice({
name: 'student',
initialState: {
name: 'Tom',
age: 20,
gender: 'Male'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
export const { setName, setAge } = studentSlice.actions
在組件中使用 useDispatch()
獲取 dispatch object,然後dispatch action 即可調用方法來操作 state:
// App.jsx
import { useSelector, useDispatch } from 'react-redux'
import { setName, setAge } from './store'
const App = () => {
const student = useSelector(state => state.student)
const dispatch = useDispatch()
const setNameHandler = () => {
dispatch(setName('Mary'))
// 等同於 dispatch({ type: 'student/setName', payload: 'Mary' })
}
const setAgeHandler = () => {
dispatch(setAge(20))
}
return (
<div>
<p>{student.name}-{student.age}-{student.gender}</p>
<button onClick={setNameHandler}>修改名字</button>
<button onClick={setAgeHandler}>修改年齡</button>
</div>
)
}
export default App
拆分 RTK 程式碼
在同一個檔案中維護多個 slice 會顯得檔案十分臃腫,寫久了也不好維護,因此建議將不同的 slice 拆分到不同的檔案中管理。
// src/store/index.js
import { createSlice, configureStore } from '@reduxjs/toolkit'
const studentSlice = createSlice({
name: 'student',
initialState: {
name: 'Tom',
age: 20,
gender: 'Male'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
const teacherSlice = createSlice({
name: 'teacher',
initialState: {
name: 'John',
age: 40,
gender: 'Male'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
export const { setName, setAge } = studentSlice.actions
export const { setName, setAge } = teacherSlice.actions
const store = configureStore({
reducer: {
student: studentSlice.reducer,
teacher: teacherSlice.reducer
}
})
export default store
像上方的程式碼中可以看出有兩個 slice,一個是 teacher
一個是 student
所以可以創建兩個檔案:
// src/store/studentSlice.js
import { createSlice } from '@reduxjs/toolkit'
const studentSlice = createSlice({
name: 'student',
initialState: {
name: 'Tom',
age: 20,
gender: 'Male'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
export const { setName, setAge } = studentSlice.actions
export const { reducer: studentReducer } = studentSlice
// src/store/teacherSlice.js
import { createSlice } from '@reduxjs/toolkit'
const teacherSlice = createSlice({
name: 'teacher',
initialState: {
name: 'John',
age: 40,
gender: 'Male'
},
reducers: {
setName(state, action) {
state.name = action.payload
},
setAge(state, action) {
state.age = action.payload
}
}
})
export const { setName, setAge } = teacherSlice.actions
export const { reducer: teacherReducer } = teacherSlice
然後原本 store/index.js
中只需要引入 studentReducer 和 teacherReducer 即可:
// src/store/index.js
import { configureStore } from '@reduxjs/toolkit'
import { studentReducer } from './studentSlice'
import { teacherReducer } from './teacherSlice'
const store = configureStore({
reducer: {
student: studentReducer,
teacher: teacherReducer
}
})
export default store
使用方法都一樣,就不多提了。
import { useSelector, useDispatch } from 'react-redux'
import { setName, setAge } from './store/studentSlice'
import { setName as setTeacherName, setAge as setTeacherAge } from './store/teacherSlice'
const App = () => {
const { student, teacher } = useSelector(state => state)
const dispatch = useDispatch()
const setStudentNameHandler = () => {
dispatch(setName('Mary'))
}
const setStudentAgeHandler = () => {
dispatch(setAge(40))
}
const setTeacherNameHandler = () => {
dispatch(setTeacherName('Zeus'))
}
const setTeacherAgeHandler = () => {
dispatch(setTeacherAge(9999))
}
return (
<div>
<p>學生信息</p>
<p>{student.name}-{student.age}-{student.gender}</p>
<button onClick={setStudentNameHandler}>修改名字</button>
<button onClick={setStudentAgeHandler}>修改年齡</button>
<p>老師信息</p>
<p>{teacher.name}-{teacher.age}-{teacher.gender}</p>
<button onClick={setTeacherNameHandler}>修改名字</button>
<button onClick={setTeacherAgeHandler}>修改年齡</button>
</div>
)
}
export default App