Query Caching
IC Reactor uses TanStack Query under the hood for intelligent caching and data synchronization. This guide covers everything you need to know about how caching works.
How It Works
Section titled “How It Works”When you call useActorQuery, IC Reactor:
- Generates a unique cache key based on the canister ID, function name, and arguments
- Returns cached data immediately if available
- Fetches fresh data in the background if data is stale
- Automatically updates the UI when new data arrives
This pattern is often called “stale-while-revalidate” and provides the best user experience by showing data immediately while keeping it fresh.
Cache Keys
Section titled “Cache Keys”Cache keys are automatically generated from your query parameters:
// Query key: ['rrkah-fqaaa...', 'getUser', 'user-123']const { data } = useActorQuery({ functionName: "getUser", args: ["user-123"],})
// Query key: ['rrkah-fqaaa...', 'getUser', 'user-456']const { data } = useActorQuery({ functionName: "getUser", args: ["user-456"],})Each unique combination of canister ID, function name, and arguments creates a separate cache entry.
Custom Query Keys
Section titled “Custom Query Keys”You can add custom keys for more granular cache control:
const { data } = useActorQuery({ functionName: "getData", args: [], queryKey: ["version", "2"], // Appended to auto-generated key})Accessing Query Keys
Section titled “Accessing Query Keys”Use generateQueryKey to get the cache key for any query:
import { backend } from "./reactor"
const queryKey = backend.generateQueryKey({ functionName: "getUser", args: ["user-123"],})// ['rrkah-fqaaa...', 'getUser', 'user-123']Stale Time
Section titled “Stale Time”staleTime determines how long data is considered fresh. Fresh data is never refetched:
// Data is fresh for 5 minutesconst { data } = useActorQuery({ functionName: "getConfig", args: [], staleTime: 1000 * 60 * 5, // 5 minutes})
// Data is never stale (static data that never changes)const { data } = useActorQuery({ functionName: "getMetadata", args: [], staleTime: Infinity,})
// Data is immediately stale (default behavior)const { data } = useActorQuery({ functionName: "getPrice", args: [], staleTime: 0, // Refetch on every mount})Garbage Collection Time
Section titled “Garbage Collection Time”gcTime determines how long unused data stays in cache before being garbage collected:
// Keep unused data for 10 minutes (default is 5 minutes)const { data } = useActorQuery({ functionName: "getUser", args: [userId], gcTime: 1000 * 60 * 10,})
// Keep data foreverconst { data } = useActorQuery({ functionName: "getConfig", args: [], gcTime: Infinity,})The difference between staleTime and gcTime:
- Stale data can be served immediately while fetching fresh data
- Garbage collected data is removed entirely and must be fetched again
Automatic Refetching
Section titled “Automatic Refetching”Refetch Interval
Section titled “Refetch Interval”Automatically refetch data at a set interval:
// Refetch every 10 secondsconst { data } = useActorQuery({ functionName: "getPrice", args: [], refetchInterval: 1000 * 10,})
// Only refetch when window is focusedconst { data } = useActorQuery({ functionName: "getPrice", args: [], refetchInterval: 1000 * 10, refetchIntervalInBackground: false,})Refetch Triggers
Section titled “Refetch Triggers”const { data } = useActorQuery({ functionName: "getNotifications", args: [], // Refetch when window regains focus (default: true) refetchOnWindowFocus: true, // Refetch when component mounts (default: true) refetchOnMount: true, // Refetch when reconnecting to network (default: true) refetchOnReconnect: true,})Manual Invalidation
Section titled “Manual Invalidation”Using QueryClient
Section titled “Using QueryClient”Invalidate queries directly with the QueryClient:
import { useQueryClient } from "@tanstack/react-query"import { backend } from "./reactor"
function RefreshButton() { const queryClient = useQueryClient()
const handleRefresh = () => { // Invalidate a specific query queryClient.invalidateQueries({ queryKey: backend.generateQueryKey({ functionName: "getUser", args: ["user-123"], }), }) }
return <button onClick={handleRefresh}>Refresh</button>}Invalidate All Canister Queries
Section titled “Invalidate All Canister Queries”// Invalidate ALL queries for this canisterbackend.invalidateQueries()Invalidate by Function Name
Section titled “Invalidate by Function Name”// Invalidate all getUser queries (any args)backend.invalidateQueries({ functionName: "getUser" })Invalidate Specific Query
Section titled “Invalidate Specific Query”// Invalidate getUser query for specific userbackend.invalidateQueries({ functionName: "getUser", args: ["user-123"],})Invalidate by Custom Query Key
Section titled “Invalidate by Custom Query Key”// Invalidate query with custom keybackend.invalidateQueries({ functionName: "getData", queryKey: ["version", "2"],})Refetch After Mutation
Section titled “Refetch After Mutation”Using invalidateQueries
Section titled “Using invalidateQueries”The simplest way to refetch related data after a mutation:
import { backend } from "./reactor"
const { mutate } = useActorMutation({ functionName: "createPost", invalidateQueries: [ backend.generateQueryKey({ functionName: "getPosts" }), backend.generateQueryKey({ functionName: "getPostCount" }), ],})Using onSuccess
Section titled “Using onSuccess”For more control, use onSuccess:
import { useQueryClient } from "@tanstack/react-query"import { backend } from "./reactor"
function CreatePost() { const queryClient = useQueryClient()
const { mutate } = useActorMutation({ functionName: "createPost", onSuccess: (newPost) => { // Invalidate posts list queryClient.invalidateQueries({ queryKey: backend.generateQueryKey({ functionName: "getPosts" }), })
// Or directly update the cache queryClient.setQueryData( backend.generateQueryKey({ functionName: "getPosts" }), (old: Post[]) => [...old, newPost] ) }, })}Optimistic Updates
Section titled “Optimistic Updates”Update the UI immediately, then sync with the server:
import { useQueryClient } from "@tanstack/react-query"import { backend } from "./reactor"
function LikeButton({ postId }: { postId: string }) { const queryClient = useQueryClient() const queryKey = backend.generateQueryKey({ functionName: "getPost", args: [postId], })
const { mutate } = useActorMutation({ functionName: "likePost", onMutate: async () => { // Cancel any outgoing refetches await queryClient.cancelQueries({ queryKey })
// Snapshot the previous value const previous = queryClient.getQueryData(queryKey)
// Optimistically update queryClient.setQueryData(queryKey, (old: Post) => ({ ...old, likes: old.likes + 1, }))
// Return context with previous value return { previous } }, onError: (err, variables, context) => { // Rollback on error if (context?.previous) { queryClient.setQueryData(queryKey, context.previous) } }, onSettled: () => { // Always refetch after error or success queryClient.invalidateQueries({ queryKey }) }, })
return <button onClick={() => mutate([postId])}>Like</button>}QueryClient Configuration
Section titled “QueryClient Configuration”Set global defaults for all queries:
import { QueryClient } from "@tanstack/query-core"
const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60, // 1 minute default gcTime: 1000 * 60 * 5, // 5 minutes retry: 2, // Retry failed queries twice retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), refetchOnWindowFocus: true, }, mutations: { retry: 1, }, },})Smart Retry Logic
Section titled “Smart Retry Logic”Don’t retry business logic errors:
import { CanisterError } from "@ic-reactor/core"
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: (failureCount, error) => { // Don't retry canister business logic errors if (error instanceof CanisterError) { return false } // Retry network errors up to 3 times return failureCount < 3 }, }, },})Prefetching
Section titled “Prefetching”Prefetch data before it’s needed:
import { backend } from "./reactor"
// In a route loaderasync function postsLoader() { await backend.queryClient.prefetchQuery( backend.getQueryOptions({ functionName: "getPosts", args: [], }) ) return null}
// Or on hoverfunction PostLink({ postId }: { postId: string }) { const handleMouseEnter = () => { backend.queryClient.prefetchQuery( backend.getQueryOptions({ functionName: "getPost", args: [postId], }) ) }
return ( <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}> View Post </Link> )}Initial Data
Section titled “Initial Data”Provide initial data while fresh data loads:
const { data } = useActorQuery({ functionName: "getSettings", args: [], initialData: { theme: "light", notifications: true, }, // Don't count initial data as fresh initialDataUpdatedAt: 0,})Placeholder Data
Section titled “Placeholder Data”Show placeholder data that doesn’t get cached:
const { data } = useActorQuery({ functionName: "getPost", args: [postId], placeholderData: { id: postId, title: "Loading...", content: "", },})Debug Caching
Section titled “Debug Caching”Enable React Query DevTools to inspect the cache:
import { ReactQueryDevtools } from "@tanstack/react-query-devtools"
function App() { return ( <QueryClientProvider client={queryClient}> <YourApp /> <ReactQueryDevtools initialIsOpen={false} /> </QueryClientProvider> )}The DevTools show:
- All cached queries
- Their status (fresh, stale, fetching)
- Cache keys
- Last updated time
- Ability to manually trigger refetches
Further Reading
Section titled “Further Reading”- TanStack Query Documentation — Complete TanStack Query docs
- Queries — IC Reactor query patterns
- Mutations — IC Reactor mutation patterns