## 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).