- Published on
使用 RTK-Query 實作 CRUD API
- Authors
- Name
- Luke Lin
RTK-Query 是一個強大的數據獲取及 cache 的工具。它可以大大的簡化 Fetch 行為的各種情境,從而解決需要自己自幹 數據獲取 及 cache 邏輯的需要,它是 Redux Toolkit 中的可選功能。
除了獲取數據之外,通常還需要進行數據更新並使 clint site 的 cache 數據與伺服器上的數據保持同步,以及需要實現應用程式中的其他行為,例如
- 監聽
Fetch行為的loading - 避免對相同的
arguments重複請求 - 即時的更新行為讓
UI感覺更快 - 在
user與UI互動時管理cache行為
Redux 核心非常精簡,由開發人員撰寫所有的邏輯,這意味著需要撰寫大量的 reducer 邏輯來管理加載狀態和 cache 數據。
Redux Toolkit 的 createAstncThunk Api 範例:
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit'
import { userAPI } from './userAPI'
// 建立 thunk
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async (userId: number, thunkAPI) => {
const response = await userAPI.fetchById(userId)
return response.data
}
)
interface UsersState {
entities: []
loading: 'idle' | 'pending' | 'succeeded' | 'failed'
}
const initialState = {
entities: [],
loading: 'idle',
} as UsersState
// 處理 reducers 中的 actions :
const usersSlice = createSlice({
name: 'users',
initialState,
reducers: {
// 標準的 reducer 邏輯中每個 reducer 都有自動生成的 action type
},
extraReducers: (builder) => {
// 為額外的 action 類型添加 reducer,並根據需要處理加載狀態
builder.addCase(fetchUserById.fulfilled, (state, action) => {
state.entities.push(action.payload)
})
},
})
// 根據需要在應用程序中 dispatch thunk
dispatch(fetchUserById(123))
為了 狀態管理 撰寫了大量的邏輯,在過去幾年裡,React 社群意識到 數據獲取和 cache實際上的考量點與 狀態管理並不相同, 我們依然可以使用 Redux 來管理狀態並 cache 數據,但可以根據不同的考量,值得專門為數據獲取而建立 Fetch 工具。
RTK-Query 從其他數據獲取工具汲取靈感,例如 Apollo Clint、React Query、URQL、SWR,但在 API 的設計上加入的獨有的方法:
- 數據獲取和
cache邏輯建構在Redux工具包createSlice和createAsyncThunkApi 之上 - Redux Toolkit 與 UI 無關,因此
RTK-Query的功能可以使用在任何UI層 - 封裝數據獲取的過程進
Reacy hooks,為組件提供data和isLoading,並在組件mounted和unMounted時管理cache數據的生命週期 - 提供 cache entry lifecycle 選項讓獲得數據後通過
websocket來更新數據的cache - 完全用
Typescript編寫,提供出色的TS使用體驗
基本用法
創建 API:
import { createApi } from '@reduxjs/toolkit/query'
/* 自動生成的 React 入口點與定義的端點對應的 hook */
import { createApi } from '@reduxjs/toolkit/query/react'
React 的基本用法,導入 createApi 之後,定義基本 URL 並定義 API 的端口:
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Pokemon } from './types'
// 定義基本 URL 和自定義的 endpoints
export const pokemonApi = createApi({
reducerPath: 'pokemonApi',
baseQuery: fetchBaseQuery({ baseUrl: 'https://pokeapi.co/api/v2/' }),
endpoints: (builder) => ({
getPokemonByName: builder.query<Pokemon, string>({
query: (name) => `pokemon/${name}`,
}),
}),
})
// Export hooks for usage in functional components, which are
// auto-generated based on the defined endpoints
export const { useGetPokemonByNameQuery } = pokemonApi
配置
加入自動生成的 reducer 和 middleware 進 store 中:
import { configureStore } from '@reduxjs/toolkit'
// Or from '@reduxjs/toolkit/query/react'
import { setupListeners } from '@reduxjs/toolkit/query'
import { pokemonApi } from './services/pokemon'
export const store = configureStore({
reducer: {
// Add the generated reducer as a specific top-level slice
[pokemonApi.reducerPath]: pokemonApi.reducer,
},
// Adding the api middleware enables caching, invalidation, polling,
// and other useful features of `rtk-query`.
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(pokemonApi.middleware),
})
// optional, but required for refetchOnFocus/refetchOnReconnect behaviors
// see `setupListeners` docs - takes an optional callback as the 2nd arg for customization
setupListeners(store.dispatch)
Use Hooks in components
到這邊就定義完基本的 API 了,RTK-Query 會處理完剩下的事情,API Slice 會生成 React Hooks 並可以在 components 中使用:
import * as React from 'react'
import { useGetPokemonByNameQuery } from './services/pokemon'
export default function App() {
// Using a query hook automatically fetches data and returns query values
const { data, error, isLoading } = useGetPokemonByNameQuery('bulbasaur')
// Individual hooks are also accessible under the generated endpoints:
// const { data, error, isLoading } = pokemonApi.endpoints.getPokemonByName.useQuery('bulbasaur')
// render UI based on data and loading state
}
常見用法
Queries
Queries 是最常見的用法。官方文件建議對於只 查詢數據 的 Get 請求可使用 Queries,而對於改變資料庫裡的數據或可能使 cache 失效的請求,應該使用 Mutation。
定義 Queries:
// Or from '@reduxjs/toolkit/query/react'
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query'
import type { Post } from './types'
const api = createApi({
baseQuery: fetchBaseQuery({
baseUrl: '/',
}),
tagTypes: ['Post'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
providesTags: ['POST'],
}),
}),
})
export const { useGetPostsQuery, useLazyGetPostsQuery } = api
在 React 中 使用 Queries:
import { Skeleton } from './Skeleton'
import { useGetPostsQuery, useLazyGetPostsQuery } from './api'
function App() {
const {
data: posts,
isFetching,
isLoading,
} = useGetPostsQuery(undefined, {
skip: false,
pollingInterval: 3000,
refetchOnMountOrArgChange: true,
})
const [useGetPostQuery, { data: post, isLoading }] = useLazyGetPostQuery()
const { post } = useGetPostsQuery(undefined, {
selectFromResult: ({ data }) => ({
post: data?.find((post) => post.id === id),
}),
})
if (isError) return <div>An error has occurred!</div>
if (isLoading) return <Skeleton />
return (
<div className={isFetching ? 'posts--disabled' : ''}>
{data.map((post) => (
<Post key={post.id} id={post.id} name={post.name} disabled={isFetching} />
))}
</div>
)
}
isLoading指的是針對 第一次運行中 的查詢。當前沒有數據可用。isFetching指的是針對端點 + 查詢參數組合的行為,不一定是第一次。數據可能來自這個hook完成的早期請求,可能使用先前的查詢參數。skip允許跳過該次re-render的查詢,默認為false當我們需要等待某些參數回來才查詢api時可以加入skip參數pollingInterval允許在提供的時間內自動重新獲取數據,默認為0refetchOnMountOrArgChange如果用戶關閉它,但隨後在 允許的時間內重新打開,將立即獲取cache的結果,並恢復之前的行為。selectFromResult從查詢結果中獲取特定的資料。
上面提供了兩種常見的查詢方式,useGetPostQuery、useLazyGetPostQuery
useGetPostQuery在mounted時就會自動獲取一次數據,之後根據監聽其arguments的異動再次自動更新數據useLazyGetPostQuery提供一個trigger函式可以手動觸發來決定何時該獲取數據
以這兩個常見用法來說我們可以思考兩個情境,簡單的考慮情境是 當使用者改變下拉選單後就需要更新資料 就可以使用 useGetPostQuery,而當 使用者改變下拉選單,並按下 Search 按鈕 後才更新資料,這時候就可以使用 useLazyQuery 來手動查詢。
Mutations
而 Mutations 則用於 更新資料庫的數據 的動作,Mutaions 還可以定義 標籤 並在需要的情境使 queries 的數據失效並強制重新獲取數據,這讓功能在實作上可以少寫很多邏輯。
定義 Mutations,實作 Posts 的 CRUD 範例,可以成為實際應用的良好基礎。
// Or from '@reduxjs/toolkit/query' if not using the auto-generated hooks
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export interface Post {
id: number
name: string
}
type PostsResponse = Post[]
export const postApi = createApi({
reducerPath: 'postsApi',
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
tagTypes: ['Posts'],
endpoints: (build) => ({
getPosts: build.query<PostsResponse, void>({
query: () => 'posts',
providesTags: ['POST'],
}),
addPost: build.mutation<Post, <Post>>({
query(body) {
return {
url: `post`,
method: 'POST',
body,
}
},
invalidatesTags: ['POST'],
}),
updatePost: build.mutation<Post, <Post>>({
query(data) {
const { id, ...body } = data
return {
url: `post/${id}`,
method: 'PUT',
body,
}
},
invalidatesTags: ['POST'],
}),
deletePost: build.mutation<{ success: boolean; id: number }, number>({
query(id) {
return {
url: `post/${id}`,
method: 'DELETE',
}
},
invalidatesTags: ['POST'],
}),
}),
})
export const {
useGetPostsQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} = postApi
import {
useGetPostsQuery,
useAddPostMutation,
useUpdatePostMutation,
useDeletePostMutation,
} from './api'
import { Post } from './type'
import { message } from 'antd'
export const Demo = () => {
const {
data: posts,
isFetching,
isLoading,
} = useGetPostsQuery(undefined, {
skip: false,
pollingInterval: 3000,
refetchOnMountOrArgChange: true,
})
const [addPost, { isLoading: isAddPostLoading }] = useAddPostMutation()
const [updatePost, { isLoading: isUpdatePostLoading }] = useUpdatePostMutation()
const [deletePost, { isLoading: isDeletePostLoading }] = useDeletePostMutation()
const handleAddPost = async (post: Post) => {
try {
// 當沒有加上 `unwrap`是不會進到 `error` 的情境的
await addPost({ id: 1, name: 'Luke Lin' }).unwrap()
message.success('success')
} catch (error) {
message.error(error.message)
}
}
return <div>...</div>
}
常見的屬性:
data- 最新返回的數據(如果存在)。如果調用同一個mutation則返回undefined直到收到新數據。error- 錯誤的結果 (如果存在) 。isUninitialized-true時代表此mutation尚未被觸發。isLoading- 是否正在等待response。isSuccess- 是否具有成功請求。isError- 最後的請求是否為錯誤狀態。reset- 重製回原始狀態並從cache中刪除當前結果。
與 useQuery 不同,useMutation 的第一個參數是一個觸發器,第二個參數則是帶有 status、error 和 data,且不會自動執行,只在觸發其 觸發器 才執行該 Mutation 當調用時,當我們希望觸發帶有 promise 的請求時可以調用帶有 unwrap 屬性的 Mutation,這將提供原始的 response 及 error使我們知道觸發的動作是成功還是失敗,可以利用它來進行 try catch 實作需要的邏輯,常見的用法可能是當 error 時觸發錯誤訊息的彈跳視窗,或是在 await 成功後往下執行想要的商業邏輯。
當使用 RTK-Query 時,
Mutations不包含Queries中的loading和fetching的差異。這是因為對於Mutations來說並不假設後續的調用與前者相關,因此要嘛就是loading要嘛就是not laoding