React Redux Toolkit(RTK) 基礎使用指南

RTK Logo

Redux 使用流程

回憶一下 Redux 的使用流程:

  1. 創建 reducer 整合函數
  2. 通過 reducer 物件創建 store
  3. 對 store 中的 state 進行訂閱
  4. 通過 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' })

若按照上方的寫法繼續寫下去,可能會出現以下問題:

  1. 如果 state 過於複雜,將會變得很難維護。
    • 解決方式:可以通過對 state 分組來解決這個問題,創建多個 reducer,然後將其合併為一個。
  2. state 每次操作時,都需要對其進行複製,然後再去修改。
  3. 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 物件的結構如下:

b3bG2c6

前面有提到 createSlice 底層有用到 createActioncreateAction 回傳的是 actionCreator 函數,調用函數才後會生成 action 物件。

而 action 物件中包含 payloadtype

  • type: slice名稱/reducer函數名稱
  • payload: 函數的參數
const { setName, setAge } = studentSlice.actions
const nameAction = setName('Olivia')
const ageAction = setAge(30)
console.log(nameAction, ageAction)
image

通常來說 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
image

store 中給了幾個 reducer 就會拿到幾個 state:

// src/store/index.js
...
const store = configureStore({
  reducer: {
    student: studentSlice.reducer,
    teacher: teacherSlice.reducer
  }
})
image

假設只需要獲取 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
oyC5lF8

拆分 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
VDd8aPy

留言

目前沒有留言。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *