Usage > Migrating to RTKQ: how to convert logic to RTKQ">Usage > Migrating to RTKQ: how to convert logic to RTKQ">
跳到主要內容

遷移到 RTK Query

您將學到什麼
  • 如何將使用 Redux Toolkit + createAsyncThunk 實作的傳統資料擷取邏輯轉換為使用 Redux Toolkit Query

概述

Redux 應用程式中副作用最常見的用例是擷取資料。Redux 應用程式通常使用 thunk、saga 或可觀察物件等工具來建立 AJAX 要求,並根據要求的結果發送動作。然後,Reducer 會偵聽這些動作來管理載入狀態並快取擷取的資料。

RTK Query 專門用於解決資料擷取的用例。雖然它無法取代所有使用 thunk 或其他副作用方法的情況,但使用 RTK Query 應該可以消除大部分手寫副作用邏輯的需求

預計 RTK Query 將涵蓋使用者先前可能使用 createAsyncThunk 的許多重疊行為,包括快取目的和要求生命週期管理(例如 isUninitializedisLoadingisError 狀態)。

若要將資料擷取功能從現有的 Redux 工具移轉到 RTK Query,應將適當的端點新增到 RTK Query API 區段,並刪除先前的功能程式碼。這通常不會包含兩個工具之間保留的許多共用程式碼,因為這些工具的工作方式不同,而且一個會取代另一個。

如果你想從頭開始使用 RTK Query,你可能還希望看到RTK Query 快速入門

範例 - 將資料擷取邏輯從 Redux Toolkit 移轉到 RTK Query

使用 Redux 實作簡單、快取的資料擷取邏輯的常見方法是使用 createSlice 設定區段,其中狀態包含查詢關聯的 datastatus,並使用 createAsyncThunk 來處理非同步要求的生命週期。以下我們將探討此類實作的範例,以及我們如何稍後將該程式碼移轉為使用 RTK Query。

注意

RTK Query 還提供了比以下所示的 thunk 範例中建立的更多功能。此範例僅用於展示如何使用 RTK Query 取代特定實作。

設計規範

對於我們的範例,工具所需的設計規範如下

  • 提供一個掛勾,使用 API 取得寶可夢資料:https://pokeapi.co/api/v2/pokemon/bulbasaur,其中 bulbasaur 可以是任何寶可夢名稱
  • 對於任何給定的名稱,只有在該工作階段尚未執行過請求時,才會傳送請求
  • 掛勾應提供我們對所提供寶可夢名稱的請求的目前狀態;無論其狀態是「未初始化」、「處理中」、「已完成」或「已拒絕」
  • 掛勾應提供我們所提供寶可夢名稱的目前資料

考量上述規格,我們首先來看看如何使用 createAsyncThunk 結合 createSlice 傳統地實作這項功能。

使用 createSlicecreateAsyncThunk 實作

Slice 檔案

以下三個片段組成我們的 slice 檔案。此檔案用於管理非同步請求生命週期,以及儲存特定寶可夢名稱的資料和請求狀態。

Thunk 動作建立器

以下我們使用 createAsyncThunk 建立一個 thunk 動作建立器,以管理非同步請求生命週期。這將可以在元件和掛勾內使用,以便傳送,以發出對某些寶可夢資料的請求。 createAsyncThunk 本身會處理請求的生命週期方法傳送:pending(處理中)、fulfilled(已完成)和 rejected(已拒絕),我們會在 slice 內處理這些方法。

src/services/pokemonSlice.ts - Thunk 動作建立器
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import type { Pokemon } from './types'
import type { RootState } from '../store'

export const fetchPokemonByName = createAsyncThunk<Pokemon, string>(
'pokemon/fetchByName',
async (name, { rejectWithValue }) => {
const response = await fetch(`https://pokeapi.co/api/v2/pokemon/${name}`)
const data = await response.json()
if (response.status < 200 || response.status >= 300) {
return rejectWithValue(data)
}
return data
},
)

// slice & selectors omitted

Slice

以下我們使用 createSlice 建立我們的 slice。我們在此定義包含請求處理邏輯的 reducer,根據我們搜尋的名稱,在我們的狀態中儲存適當的「狀態」和「資料」。

src/services/pokemonSlice.ts - slice 邏輯
// imports & thunk action creator omitted

type RequestState = 'pending' | 'fulfilled' | 'rejected'

export const pokemonSlice = createSlice({
name: 'pokemon',
initialState: {
dataByName: {} as Record<string, Pokemon | undefined>,
statusByName: {} as Record<string, RequestState | undefined>,
},
reducers: {},
extraReducers: (builder) => {
// When our request is pending:
// - store the 'pending' state as the status for the corresponding pokemon name
builder.addCase(fetchPokemonByName.pending, (state, action) => {
state.statusByName[action.meta.arg] = 'pending'
})
// When our request is fulfilled:
// - store the 'fulfilled' state as the status for the corresponding pokemon name
// - and store the received payload as the data for the corresponding pokemon name
builder.addCase(fetchPokemonByName.fulfilled, (state, action) => {
state.statusByName[action.meta.arg] = 'fulfilled'
state.dataByName[action.meta.arg] = action.payload
})
// When our request is rejected:
// - store the 'rejected' state as the status for the corresponding pokemon name
builder.addCase(fetchPokemonByName.rejected, (state, action) => {
state.statusByName[action.meta.arg] = 'rejected'
})
},
})

// selectors omitted

Selector

以下我們定義了選擇器,讓我們稍後可以存取任何給定寶可夢名稱的適當狀態和資料。

src/services/pokemonSlice.ts - 選擇器
// imports, thunk action creator & slice omitted

export const selectStatusByName = (state: RootState, name: string) =>
state.pokemon.statusByName[name]
export const selectDataByName = (state: RootState, name: string) =>
state.pokemon.dataByName[name]

儲存

在我們應用程式的儲存中,我們在狀態樹中的pokemon分支下包含了切片中對應的簡化器。這讓我們儲存可以處理我們在執行應用程式時會發出的請求的適當動作,使用先前定義的邏輯。

src/services/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { pokemonSlice } from './services/pokemonSlice'

export const store = configureStore({
reducer: {
pokemon: pokemonSlice.reducer,
},
})

export type RootState = ReturnType<typeof store.getState>

為了讓儲存可以在我們的應用程式中存取,我們會使用來自react-reduxProvider元件包裝我們的App元件。

src/index.ts
import { render } from 'react-dom'
import { Provider } from 'react-redux'

import App from './App'
import { store } from './store'

const rootElement = document.getElementById('root')
render(
<Provider store={store}>
<App />
</Provider>,
rootElement,
)

自訂勾子

以下我們建立一個勾子來管理在適當的時間傳送我們的請求,以及從儲存取得適當的資料和狀態。useDispatchuseSelector來自react-redux,用於與 Redux 儲存溝通。在我們的勾子結束時,我們會將資訊回傳成一個整齊的封裝物件,讓元件可以存取。

src/hooks.ts
import { useEffect } from 'react'
import { useSelector } from 'react-redux'
import { useAppDispatch } from './store'
import type { RootState } from './store'
import {
fetchPokemonByName,
selectStatusByName,
selectDataByName,
} from './services/pokemonSlice'

export function useGetPokemonByNameQuery(name: string) {
const dispatch = useAppDispatch()
// select the current status from the store state for the provided name
const status = useSelector((state: RootState) =>
selectStatusByName(state, name)
)
// select the current data from the store state for the provided name
const data = useSelector((state: RootState) => selectDataByName(state, name))
useEffect(() => {
// upon mount or name change, if status is uninitialized, send a request
// for the pokemon name
if (status === undefined) {
dispatch(fetchPokemonByName(name))
}
}, [status, name, dispatch])

// derive status booleans for ease of use
const isUninitialized = status === undefined
const isLoading = status === 'pending' || status === undefined
const isError = status === 'rejected'
const isSuccess = status === 'fulfilled'

// return the import data for the caller of the hook to use
return { data, isUninitialized, isLoading, isError, isSuccess }
}

使用自訂勾子

我們上面的程式碼符合所有的設計規範,所以讓我們使用它!以下我們可以看到如何呼叫元件中的勾子,並回傳相關的資料和狀態布林值。

我們下面的實作在元件中提供了以下行為

  • 當我們的元件掛載時,如果尚未針對提供的寶可夢名稱傳送該階段的請求,則發送請求
  • 勾子會在可用時持續提供最新接收的資料,以及請求狀態布林值isUninitializedisPendingisFulfilledisRejected,以便根據我們的狀態在任何給定的時刻確定目前的使用者介面。
src/App.tsx
import * as React from 'react'
import { useGetPokemonByNameQuery } from './hooks'

export default function App() {
const { data, isError, isLoading } = useGetPokemonByNameQuery('bulbasaur')

return (
<div className="App">
{isError ? (
<>Oh no, there was an error</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<h3>{data.species.name}</h3>
<img src={data.sprites.front_shiny} alt={data.species.name} />
</>
) : null}
</div>
)
}

可以在下面看到上述程式碼的可執行範例

轉換成 RTK Query

我們上面的實作確實完美符合指定的條件,但是,擴充程式碼以包含進一步的端點可能會涉及很多重複。它也有一些可能不會立即顯而易見的特定限制。例如,同時呈現多個元件呼叫我們的勾子會同時針對妙蛙種子發送請求!

以下我們將逐步說明如何透過將上述程式碼遷移至使用 RTK Query 來避免大量樣板程式碼。RTK Query 也會為我們處理許多其他情況,包括在更細緻的層級上對重複請求進行重複資料刪除,以防止傳送不必要的重複請求,例如上述提出的那樣。

API Slice 檔案

以下我們的程式碼用於我們的 API slice 定義。它作為我們的網路 API 介面層,並使用 createApi 建立。此檔案將包含我們的端點定義,而 createApi 會提供我們一個自動產生的勾子,它僅在必要時管理觸發我們的請求,並提供請求狀態生命週期布林值。

這將完全涵蓋我們為整個 slice 檔案實作的邏輯,包括 thunk、slice 定義、選取器、我們的自訂勾子!

src/services/api.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'

export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
reducerPath: 'pokemonApi',
endpoints: (build) => ({
getPokemonByName: build.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})

export const { useGetPokemonByNameQuery } = api

將 API slice 連接到儲存體

現在我們已經建立了 API 定義,我們需要將它連接到我們的儲存體。為此,我們需要使用我們建立的 api 中的 reducerPathmiddleware 屬性。這將允許儲存體處理產生的勾子所使用的內部動作,允許產生的 API 邏輯正確地找到狀態,並新增管理快取、失效、訂閱、輪詢等的邏輯。

src/store.ts
import { configureStore } from '@reduxjs/toolkit'
import { pokemonSlice } from './services/pokemonSlice'
import { api } from './services/api'

export const store = configureStore({
reducer: {
pokemon: pokemonSlice.reducer,
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})

export type RootState = ReturnType<typeof store.getState>

使用我們自動產生的勾子

在這個基本層級,自動產生的勾子的用法與我們的自訂勾子相同!我們只需要變更我們的匯入路徑,就可以開始使用了!

src/App.tsx
  import * as React from 'react'
- import { useGetPokemonByNameQuery } from './hooks'
+ import { useGetPokemonByNameQuery } from './services/api'

export default function App() {
const { data, isError, isLoading } = useGetPokemonByNameQuery('bulbasaur')


return (
<div className="App">
{isError ? (
<>Oh no, there was an error</>
) : isLoading ? (
<>Loading...</>
) : data ? (
<>
<h3>{data.species.name}</h3>
<img src={data.sprites.front_shiny} alt={data.species.name} />
</>
) : null}
</div>
)
}

清除未使用的程式碼

如前所述,我們的 api 定義已經取代了我們先前使用 createAsyncThunkcreateSlice 和我們的自訂勾子定義實作的所有邏輯。

由於我們不再使用那個 slice,我們可以從我們的儲存體中移除匯入和 reducer

src/store.ts
  import { configureStore } from '@reduxjs/toolkit'
- import { pokemonSlice } from './services/pokemonSlice'
import { api } from './services/api'


export const store = configureStore({
reducer: {
- pokemon: pokemonSlice.reducer,
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})

export type RootState = ReturnType<typeof store.getState>

我們也可以完全移除整個 slice 和勾子檔案

- src/services/pokemonSlice.ts (-51 lines)
- src/hooks.ts (-34 lines)

我們現在已使用不到 20 行程式碼重新實作完整的設計規範(甚至更多!),並可透過在 API 定義中新增額外端點輕鬆擴充。

以下是可以看見使用 RTK Query 重新調整實作的執行範例