## Redux prefetch
I want to write a blog post about prefetching data using RTK-query. We are building an ecommerce site and when navigation across products and categories, we want to minimize the loading screens and basically have the products/categories loaded before we navigate to them.
First, we want to write a paragraph that explains what RTK query is, and what prefetching is good for (in this case, we want to avoid reflashing before navigating to a new page). We also want to describe how the rtk query cache mechanism works.
---
Here is some general information about RTK query cache and prefetch:
# RTK Query Prefetching
Prefetching is a technique used to load data in advance of its use, improving user experience by reducing wait times. It's particularly useful in scenarios such as hovering over navigation elements or preparing data for components on an upcoming page.
## Using React Hooks for Prefetching
### `usePrefetch` Hook Signature
The `usePrefetch` hook is designed to fetch data before it's needed. It returns a trigger function and accepts an endpoint name and optional parameters to control the prefetching behavior.
```typescript
usePrefetch(endpointName, options?): (arg, options?) => void;
```
### Customizing Hook Behavior
The behavior of the `usePrefetch` hook can be customized with options such as `force` and `ifOlderThan`. These options determine when the prefetch should occur, with `force` ignoring the cache and `ifOlderThan` considering the age of the cached data.
### Trigger Function Behavior
The trigger function from `usePrefetch` always returns `void`. It follows specific rules based on the options provided, such as ignoring the cache if `force` is true or only fetching if the cache is outdated when `ifOlderThan` is set.
```tsx
function User() {
const prefetchUser = usePrefetch('getUser');
return (
<div>
<button onMouseEnter={() => prefetchUser(4, { ifOlderThan: 35 })}>
Low priority
</button>
<button onMouseEnter={() => prefetchUser(4, { force: true })}>
High priority
</button>
</div>
);
}
```
## Prefetching Without React Hooks
You can also prefetch data without using the `usePrefetch` hook by dispatching a prefetch thunk or initiating a query action directly. This requires manual handling of the prefetch logic.
```typescript
store.dispatch(api.util.prefetch(endpointName, arg, { force: false, ifOlderThan: 10 }));
```
## Prefetching Strategies
### Basic Prefetching
Basic prefetching involves loading data when a user interacts with an element, such as hovering over a button. This can be done using the `usePrefetch` hook or by dispatching actions manually.
### Automatic Prefetching
Automatic prefetching loads the next set of data without user interaction, creating a seamless experience. This can be implemented by triggering prefetch actions based on the current state or user journey.
### Prefetching All Known Pages
For a more advanced strategy, you can prefetch all known pages or data after the initial load. This ensures that all potential data the user might need is fetched in advance, reducing loading times as they navigate.
```typescript
usePrefetchImmediately('getUser', 5);
```
### RTK Query Cache Behaviour
##### Subscription and Serialization
When a component requests data from an API endpoint, RTK Query serializes the request parameters into a unique `queryCacheKey`. Any future requests with identical parameters will use the cached data associated with that key. This process is known as de-duplication and ensures that multiple components can share the same cached data.
##### Cache Lifetime
Cached data has a default lifetime determined by the number of active subscriptions to it. As long as there is at least one active subscription, the data remains in the cache. When the last subscription is removed, the data is scheduled for removal after a default period (60 seconds), which can be customized.
#### Manipulating Cache Behavior
##### Adjusting Cache Lifetime (`keepUnusedDataFor`)
You can control how long data stays in the cache after all subscriptions end by setting the `keepUnusedDataFor` option. This can be set globally for all endpoints or individually per endpoint.
```ts
// Global setting
createApi({
keepUnusedDataFor: 30, // 30 seconds
// ... other settings
})
// Per-endpoint setting
builder.query({
keepUnusedDataFor: 5, // 5 seconds
// ... other query settings
})
```
##### Forcing Data Re-fetch (`refetch` and `initiate`)
To manually trigger a data re-fetch, use the `refetch` method from a query hook or dispatch the `initiate` action with `forceRefetch: true`.
```tsx
// Using refetch from a hook
const { refetch } = useGetPostsQuery({ count: 5 });
refetch();
// Dispatching initiate action
dispatch(api.endpoints.getPosts.initiate({ count: 5 }, { forceRefetch: true }));
```
##### Encouraging Re-fetching (`refetchOnMountOrArgChange`)
You can encourage data to be re-fetched more frequently by setting `refetchOnMountOrArgChange` to `true` or a number of seconds. This can be set globally or per hook call.
```ts
// Global setting
createApi({
refetchOnMountOrArgChange: 30, // 30 seconds
// ... other settings
})
// Per hook call
useGetPostsQuery({ count: 5 }, { refetchOnMountOrArgChange: true })
```
##### Re-fetching on Window Focus (`refetchOnFocus`)
To automatically re-fetch data when the application window regains focus, set `refetchOnFocus` to `true`. This requires `setupListeners` to be called.
```ts
// Global setting
createApi({
refetchOnFocus: true,
// ... other settings
})
// Enable listener behavior
setupListeners(store.dispatch)
```
##### Re-fetching on Network Reconnection (`refetchOnReconnect`)
Similar to `refetchOnFocus`, `refetchOnReconnect` will trigger a re-fetch of all subscribed queries when the network connection is restored.
```ts
// Global setting
createApi({
refetchOnReconnect: true,
// ... other settings
})
// Enable listener behavior
setupListeners(store.dispatch)
```
##### Invalidating Cache Tags
RTK Query can automatically re-fetch queries when related data is mutated by using a system of cache tags. When a mutation occurs, any queries with matching tags will be refetched.
#### Tradeoffs and Considerations
RTK Query does not use a normalized cache to deduplicate identical items across requests. This design choice simplifies the cache management but means that identical data may be stored multiple times in the cache. However, using cache tags can help maintain consistency across queries.
#### Practical Examples
##### Cache Subscription Lifetime Demo
This interactive demo illustrates how the cache behaves with multiple components subscribing to the same data. It shows how the cache retains data based on active subscriptions and the `keepUnusedDataFor` setting.
```tsx
// Example components using the same query
function ComponentOne() {
const { data } = useGetUserQuery(1);
// ...
}
function ComponentTwo() {
const { data } = useGetUserQuery(2);
// ...
}
```
When both components are unmounted, the cached data will be removed after the time specified by `keepUnusedDataFor`.
---
Then, We want to show our actual endpoint definitions, and how we get a prefetch helper. While we do use 3 prefetch operations, the example for the blog post should be simplified.
```typescript
export const categoryApi = createApi({
reducerPath: 'categoryApi',
baseQuery: fetchBaseQuery({
baseUrl: BASE_URL,
}),
endpoints: (builder) => ({
getCategory: builder.query<ICategory, string>({
query: (url: string) => `/category/${url}`,
}),
getCategoryProductsCount: builder.query<number, { url: string; params?: any }>({
query: (payload: { url: string; params?: any }) => ({
url: `/category/${payload.url}/products/count`,
params: payload.params,
}),
}),
getCategoryProducts: builder.query<IProduct[], { url: string; params?: any }>({
query: (payload: { url: string; params?: any }) => ({
url: `/category/${payload.url}/products`,
params: payload.params,
}),
}),
}),
})
export const {
useGetCategoryQuery,
useGetCategoryProductsCountQuery,
useGetCategoryProductsQuery,
usePrefetch
} = categoryApi
```
We should explain what usePrefetch does.
Here is the part where we use it (again, we want to simplify this to be only a single link to navigate to a category, we want to leave the products counts and other information out of the way).
```typescript
const { usePrefetch } = categoryApi
const filterState = useAppSelector(filterStateSelector)
const prefetchCategory = usePrefetch('getCategory')
const prefetchCategoryProductsCount = usePrefetch('getCategoryProductsCount')
const prefetchCategoryProducts = usePrefetch('getCategoryProducts')
return (
<ContainerStyled>
<CategoryItemStyled className='category-item'>
<BreadCrumbs flow={BREAD_CRUMBS_FLOW} />
<CategoryItemHeadeStyled className='category-item-header'>
<div className='category-item-header__title'>{categoryData.name}</div>
<div className='category-item-header__sub-title'>
{categoryData.subCategories.length} products
</div>
</CategoryItemHeadeStyled>
{categoryData.hasSubCategories && (
<CategoryMainStyled className='category-main'>
<div className='category-main__product-list'>
{visibleSubCategories.map(
({ id, image, dynamicUrl, urlName, name, shortDescription }) => (
<div className='category-main__product-item' key={id}>
<Link
to={`/${dynamicUrl}`}
onMouseEnter={() => {
console.log('urlName', urlName, 'dynamicUrl', dynamicUrl)
prefetchCategory(urlName)
prefetchCategoryProductsCount({ url: urlName, params: filterState })
prefetchCategoryProducts({ url: urlName, params: filterState })
}}
>
<div className='category-main__product-image-container'>
<Media
className='category-main__product-image'
src={getMediaUrlByType(image, 'categoryimages')}
alt={name}
/>
</div>
</Link>
<div className='category-main__product-title'>
<Link to={`/${dynamicUrl}`}>{name}</Link>
</div>
<div className='category-main__product-description'>{shortDescription}</div>
</div>
),
```
---
Now write the article. Remember:
First a paragraph about RTK query.
Then a paragraph about prefetch and caching.
Then a paragraph about our API endpoints and usePrefetch (with code example).
Then a paragraph about our prefetch links and how they are used in our category page (with code example).