快取行為
RTK Query 的一項主要功能是管理快取資料。當資料從伺服器擷取時,RTK Query 會將資料儲存在 Redux store 中,作為「快取」。當針對相同資料執行其他要求時,RTK Query 會提供現有的快取資料,而不是將其他要求傳送至伺服器。
RTK Query 提供許多概念和工具,用於操作快取行為並根據您的需求調整快取行為。
預設快取行為
在 RTK Query 中,快取基於
- API 端點定義
- 當組件訂閱來自端點的資料時所使用的序列化查詢參數
- 主動訂閱參考計數
當訂閱開始時,與端點一起使用的參數會序列化並內部儲存為請求的 queryCacheKey
。任何產生相同 queryCacheKey
的未來請求(即使用相同的參數呼叫,序列化係數)將與原始請求重複,並將共用相同的資料和更新。也就是說,執行相同請求的兩個獨立元件將使用相同的快取資料。
當嘗試請求時,如果資料已存在快取中,則會提供該資料,且不會向伺服器傳送新的請求。否則,如果資料不存在快取中,則會傳送新的請求,並將傳回的回應儲存在快取中。
訂閱是參考計數的。要求相同端點+參數的其他訂閱會增加參考計數。只要資料有作用中的「訂閱」(例如,如果已掛載呼叫端點的 useQuery
掛鉤的元件),資料就會保留在快取中。一旦移除訂閱(例如,當最後訂閱資料的元件卸載時),經過一段時間(預設 60 秒)後,資料將從快取中移除。過期時間可以使用 API 定義整體 的 keepUnusedDataFor
屬性,以及 每個端點 的屬性來設定。
快取生命週期與訂閱範例
想像一個端點,它預期 id
作為查詢參數,以及 4 個已掛載的元件,要求從這個相同的端點取得資料
import { useGetUserQuery } from './api.ts'
function ComponentOne() {
// component subscribes to the data
const { data } = useGetUserQuery(1)
return <div>...</div>
}
function ComponentTwo() {
// component subscribes to the data
const { data } = useGetUserQuery(2)
return <div>...</div>
}
function ComponentThree() {
// component subscribes to the data
const { data } = useGetUserQuery(3)
return <div>...</div>
}
function ComponentFour() {
// component subscribes to the *same* data as ComponentThree,
// as it has the same query parameters
const { data } = useGetUserQuery(3)
return <div>...</div>
}
當四個元件訂閱端點時,端點 + 查詢參數只會有三種不同的組合。查詢參數 1
和 2
各有一個訂閱者,而查詢參數 3
有兩個訂閱者。RTK Query 會執行三次不同的擷取;每個端點的每個唯一查詢參數集合一次。
資料會保留在快取中,只要至少有一個作用中的訂閱者有興趣於該端點 + 參數組合。當訂閱者參考計數達到零時,會設定一個計時器,如果在計時器到期之前沒有新的訂閱該資料,快取資料將會移除。預設過期時間為 60 秒,可以同時為 API 定義整體 和 每個端點 設定。
如果在上述範例中卸載「ComponentThree」,無論經過多少時間,資料仍會保留在快取中,因為「ComponentFour」仍訂閱相同的資料,而訂閱參考計數會是 1
。不過,一旦「ComponentFour」卸載,訂閱參考計數就會變成 0
。資料會在剩餘的過期時間內保留在快取中。如果在計時器到期前沒有建立新的訂閱,快取資料最終會被移除。
操作快取行為
除了預設行為之外,RTK Query 還提供多種方法,可以在資料應該視為無效或適合「重新整理」的其他情況下,提早重新擷取資料。
使用 keepUnusedDataFor
減少訂閱時間
如上文 預設快取行為 和 快取生命週期與訂閱範例 所述,預設情況下,資料會在訂閱參考計數降為零後,保留在快取中 60 秒。
可以使用 API 定義和每個端點的 keepUnusedDataFor
選項來設定此值。請注意,如果提供了每個端點版本,它將會覆寫 API 定義中的設定。
將數字(以秒為單位)提供給 keepUnusedDataFor
,指定訂閱參考計數降為零後,資料應保留在快取中的時間長度。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
keepUnusedDataFor: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
// configuration for an individual endpoint, overriding the api setting
keepUnusedDataFor: 5,
}),
}),
})
使用 refetch
/initiate
按需重新擷取
為了完全細緻地控制重新擷取資料,可以使用 refetch
函式,它是從 useQuery
或 useQuerySubscription
鉤子回傳的結果屬性。
呼叫 refetch
函式會強制重新擷取關聯的查詢。
或者,你可以傳送端點的 initiate
thunk 動作,將選項 forceRefetch: true
傳遞給 thunk 動作建立器以產生相同的效應。
import { useDispatch } from 'react-redux'
import { useGetPostsQuery } from './api'
const Component = () => {
const dispatch = useDispatch()
const { data, refetch } = useGetPostsQuery({ count: 5 })
function handleRefetchOne() {
// force re-fetches the data
refetch()
}
function handleRefetchTwo() {
// has the same effect as `refetch` for the associated query
dispatch(
api.endpoints.getPosts.initiate(
{ count: 5 },
{ subscribe: false, forceRefetch: true },
),
)
}
return (
<div>
<button onClick={handleRefetchOne}>Force re-fetch 1</button>
<button onClick={handleRefetchTwo}>Force re-fetch 2</button>
</div>
)
}
使用 refetchOnMountOrArgChange
鼓勵重新擷取
查詢可以透過 refetchOnMountOrArgChange
屬性鼓勵比平常更頻繁地重新擷取。這可以傳遞給整個端點、個別的 hook 呼叫,或在傳送 initiate
動作時(動作建立器的選項名稱為 forceRefetch
)。
refetchOnMountOrArgChange
用於在其他情況中鼓勵重新擷取,而預設行為會提供快取資料。
refetchOnMountOrArgChange
接受布林值或數字作為秒數。
傳遞 false
(預設值)給此屬性將使用 上面描述的 預設行為。
傳遞 true
給此屬性將導致在新增查詢的新訂閱者時,端點總是重新擷取。如果傳遞給個別的 hook 呼叫,而不是 API 定義本身,則這只適用於該 hook 呼叫。也就是說,當呼叫 hook 的元件掛載或引數變更時,它將總是重新擷取,無論端點 + 引數組合的快取資料是否已存在。
傳遞 數字
作為秒數值將使用下列行為
- 在建立查詢訂閱時
- 如果快取中已有查詢,它將比較目前時間和該查詢最後完成的時間戳記,
- 如果提供的秒數已過,它將重新擷取。
- 如果沒有查詢,它將擷取資料。
- 如果已有查詢,但自上次查詢以來指定的秒數尚未經過,它將提供現有的快取資料。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnMountOrArgChange: 30,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
import { useGetPostsQuery } from './api'
const Component = () => {
const { data } = useGetPostsQuery(
{ count: 5 },
// this overrules the api definition setting,
// forcing the query to always fetch when this component is mounted
{ refetchOnMountOrArgChange: true },
)
return <div>...</div>
}
使用 refetchOnFocus
在視窗焦點時重新擷取
refetchOnFocus
選項可讓您控制 RTK Query 是否在應用程式視窗重新取得焦點後嘗試重新擷取所有已訂閱的查詢。
如果您在 skip: true
旁指定此選項,則在 skip 為 false 之前不會評估此選項。
請注意,這需要呼叫 setupListeners
。
此選項可在 API 定義中使用 createApi
,以及 useQuery
、useQuerySubscription
、useLazyQuery
和 useLazyQuerySubscription
勾子中使用。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnFocus: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
使用 refetchOnReconnect
重新擷取網路重新連線
createApi
上的 refetchOnReconnect
選項可讓您控制 RTK Query 是否在重新取得網路連線後嘗試重新擷取所有已訂閱的查詢。
如果您在 skip: true
旁指定此選項,則在 skip
為 false 之前不會評估此選項。
請注意,這需要呼叫 setupListeners
。
此選項可在 API 定義中使用 createApi
,以及 useQuery
、useQuerySubscription
、useLazyQuery
和 useLazyQuerySubscription
勾子中使用。
- TypeScript
- JavaScript
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
import type { Post } from './types'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query<Post[], number>({
query: () => `posts`,
}),
}),
})
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'
export const api = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/' }),
// global configuration for the api
refetchOnReconnect: true,
endpoints: (builder) => ({
getPosts: builder.query({
query: () => `posts`,
}),
}),
})
- TypeScript
- JavaScript
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
export type RootState = ReturnType<typeof store.getState>
import { configureStore } from '@reduxjs/toolkit'
import { setupListeners } from '@reduxjs/toolkit/query'
import { api } from './services/api'
export const store = configureStore({
reducer: {
[api.reducerPath]: api.reducer,
},
middleware: (gDM) => gDM().concat(api.middleware),
})
// enable listener behavior for the store
setupListeners(store.dispatch)
透過使快取標籤失效來在變更後重新擷取
RTK Query 使用一個選用的 快取標籤 系統,以自動重新擷取資料受到變更端點影響的查詢端點。
請參閱 自動重新擷取 以取得此概念的完整詳細資料。
權衡
沒有正規化或去重快取
RTK Query 故意不實作會在多個要求中對相同項目去重的快取。原因有幾個
- 一個完全正規化且跨查詢共用的快取是一個困難的問題
- 我們目前沒有時間、資源或興趣去嘗試解決這個問題
- 在許多情況下,當資料失效時重新擷取資料就能順利運作,而且也比較容易理解
- RTKQ 至少可以協助解決「擷取一些資料」的一般使用案例,而這對許多人來說都是一個很大的痛點
舉例來說,假設我們有一個 API 區段,其中包含 getTodos
和 getTodo
端點,而我們的元件會執行以下查詢
getTodos()
getTodos({filter: 'odd'})
getTodo({id: 1})
這些查詢結果中的每個結果都會包含一個 Todo 物件,其外觀類似於 {id: 1}
。
在一個完全正規化的去重快取中,此 Todo 物件只會儲存一個單一副本。然而,RTK Query 會將每個查詢結果獨立儲存在快取中。因此,這將導致此 Todo 的三個獨立副本快取在 Redux 儲存區中。然而,如果所有端點持續提供相同的標籤(例如 {type: 'Todo', id: 1}
),那麼使該標籤失效將強制所有相符的端點重新擷取其資料以維持一致性。
Redux 文件始終建議 將資料保存在正規化的查詢表中 以便輕鬆透過 ID 尋找項目並在儲存區中更新項目,而 RTK 的 createEntityAdapter
則旨在協助管理正規化的狀態。這些概念仍然有價值且不會消失。然而,如果您使用 RTK Query 來管理快取資料,則較不需要自己以這種方式操作資料。
這裡有幾個額外的重點可以提供協助
- 產生的查詢掛勾具有 一個
selectFromResult
選項,允許元件從查詢結果中讀取個別資料片段。舉例來說,一個<TodoList>
元件可能會呼叫useTodosQuery()
,而每個個別<TodoListItem>
都可以使用相同的查詢掛勾,但從結果中選取以取得正確的 todo 物件。 - 您可以使用
transformResponse
端點選項 來修改擷取的資料,使其 儲存在不同的形狀中,例如在將資料插入快取之前,使用createEntityAdapter
來正規化 此一回應 的資料。
進一步資訊
範例
快取訂閱生命週期示範
此範例是訂閱者參考計數和 keepUnusedDataFor
的值如何彼此互動的實際示範。Subscriptions
和 Queries
(包括快取資料)會顯示在示範中供您視覺化(請注意,這也可以在 Redux Devtools Extension 中檢視)。
掛載兩個元件,每個元件都具有相同的端點查詢(useGetUsersQuery(2)
)。您將能夠觀察到,當切換元件時,訂閱者參考計數將會減少。在切換兩個元件後,訂閱者參考計數達到 0,您將觀察到 Queries
區段下的快取資料將持續 5 秒(此示範中為端點提供的 keepUnusedDataFor
值)。如果訂閱者參考計數在整個期間保持為 0,則快取資料將從儲存區中移除。