Advanced Features & Optimization
Learn advanced techniques for building performant, scalable server state management with Zustic Query.
Caching Architecture
Zustic Query uses an intelligent time-based caching system where each endpoint maintains its own cache expiration timer, allowing fine-grained control over data freshness and network efficiency.
Cache Flow & Behavior
The caching mechanism follows a predictable lifecycle:
const api = createApi({
baseQuery: myBaseQuery,
cacheTimeout: 5 * 60 * 1000, // Cache for 5 minutes
endpoints: (builder) => ({
getUsers: builder.query({
query: () => ({ url: '/users' })
})
})
})
// First call: Network request, store in cache
const { data: users1 } = useGetUsersQuery()
// Within 5 minutes: Return cached data instantly
const { data: users2 } = useGetUsersQuery()
// After 5 minutes: Fetch fresh data
const { data: users3 } = useGetUsersQuery()
// Manual refetch: Bypass cache immediately
const { reFetch } = useGetUsersQuery()
reFetch() // Always fetches fresh
Cache Configuration Strategies
Real-Time Data (Short Cache)
For frequently changing data, use a short cache window:
const api = createApi({
baseQuery: myBaseQuery,
cacheTimeout: 1 * 60 * 1000, // 1 minute
endpoints: (builder) => ({
getLiveStats: builder.query({
query: () => ({ url: '/stats' })
})
})
})
Stable Data (Long Cache)
For reference data that rarely changes, use extended cache durations:
const api = createApi({
baseQuery: myBaseQuery,
cacheTimeout: 30 * 60 * 1000, // 30 minutes
endpoints: (builder) => ({
getCountries: builder.query({
query: () => ({ url: '/countries' })
})
})
})
Always Fresh (No Cache)
For data that must always be current, disable caching entirely:
const api = createApi({
baseQuery: myBaseQuery,
cacheTimeout: 0, // Disable cache
endpoints: (builder) => ({
getRandomNumber: builder.query({
query: () => ({ url: '/random' })
})
})
})
Manual Cache Invalidation
Use reFetch() to bypass the cache and retrieve fresh data immediately. This is essential for operations like user-initiated refreshes or after data modifications.
Refresh Button Example
export function UsersList() {
const { data, reFetch, isLoading } = useGetUsersQuery()
const handleRefresh = () => {
reFetch() // Bypass cache, fetch fresh data
}
return (
<div>
<button onClick={handleRefresh} disabled={isLoading}>
{isLoading ? 'Refreshing...' : ' Refresh'}
</button>
<ul>
{data?.map(user => <li key={user.id}>{user.name}</li>)}
</ul>
</div>
)
}
Data Transformation
Transform API responses into application-specific formats, enabling clean separation between server contracts and application logic.
Response Normalization
interface ApiUser {
id: number
first_name: string
last_name: string
created_at: string
}
interface AppUser {
id: number
fullName: string
joinDate: Date
}
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => ({ url: `/users/${id}` }),
// Transform API format to app format
transformResponse: (data: ApiUser): AppUser => {
return {
id: data.id,
fullName: `${data.first_name} ${data.last_name}`,
joinDate: new Date(data.created_at)
}
}
})
})
// Component receives transformed data
export function UserDetail({ userId }: { userId: number }) {
const { data: user } = useGetUserQuery(userId)
return (
<div>
<h1>{user?.fullName}</h1>
<p>Joined: {user?.joinDate.toLocaleDateString()}</p>
</div>
)
}
Error Handling & Recovery
Implement robust error handling with transformation and automatic retry strategies.
Error Normalization
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => ({ url: `/users/${id}` }),
// Normalize error messages
transformError: (error: string) => {
if (error.includes('404')) return 'User not found'
if (error.includes('401')) return 'Unauthorized'
if (error.includes('500')) return 'Server error'
return 'An error occurred'
}
})
})
Automatic Retry with Exponential Backoff
Improve reliability by automatically retrying failed requests with progressive delays:
const retryMiddleware = async (ctx, next) => {
let lastError
// Try up to 3 times
for (let i = 0; i < 3; i++) {
const result = await next()
if (!result.error) {
return result
}
lastError = result.error
// Exponential backoff: 1s, 2s, 4s
if (i < 2) {
await new Promise(r => setTimeout(r, Math.pow(2, i) * 1000))
}
}
return { error: lastError }
}
const api = createApi({
baseQuery: myBaseQuery,
middlewares: [retryMiddleware],
endpoints: (builder) => ({
getUser: builder.query({
query: (id) => ({ url: `/users/${id}` })
})
})
})
Sequential Data Dependencies
Implement dependent query patterns where subsequent requests only execute after prerequisite data is loaded, preventing unnecessary network overhead.
Multi-Step Data Loading
export function UserPosts({ userId }: { userId: number }) {
// First query: fetch user
const { data: user } = useGetUserQuery(userId)
// Second query: depends on user being loaded
const { data: posts } = useGetUserPostsQuery(user?.id ?? 0, {
skip: !user // Skip until user is loaded
})
return (
<div>
<h1>{user?.name}</h1>
{posts?.map(post => (
<article key={post.id}>{post.title}</article>
))}
</div>
)
}
Real-Time Data Updates
Implement polling patterns for data that requires frequent refresh cycles, such as live statistics or status feeds.
Polling Implementation
import { useEffect } from 'react'
export function LiveStats() {
const { data: stats, reFetch } = useGetStatsQuery()
// Poll every 5 seconds
useEffect(() => {
const interval = setInterval(() => {
reFetch()
}, 5000)
return () => clearInterval(interval)
}, [reFetch])
return <div>Count: {stats?.count}</div>
}
Optimized Network Requests
Reduce network overhead and improve performance by batching multiple individual requests into single batch operations.
Batching Multiple Resources
// Problem: Multiple individual requests
export function Users() {
const { data: user1 } = useGetUserQuery(1)
const { data: user2 } = useGetUserQuery(2)
const { data: user3 } = useGetUserQuery(3)
// 3 separate requests
}
// Solution: Batch endpoint
const api = createApi({
baseQuery: myBaseQuery,
endpoints: (builder) => ({
getUsersBatch: builder.query({
query: (ids: number[]) => ({
url: '/users/batch',
method: 'POST',
body: { ids }
})
})
})
})
export function Users() {
const { data: users } = useGetUsersBatchQuery([1, 2, 3])
// 1 request for all users
}
Computational Memoization
Cache expensive computations to prevent redundant calculations across re-renders, improving application responsiveness and memory efficiency.
Memoizing Derived State
import { useMemo } from 'react'
export function UsersList() {
const { data: users } = useGetUsersQuery()
// Memoize expensive computations
const sortedUsers = useMemo(() => {
return users?.sort((a, b) => a.name.localeCompare(b.name)) || []
}, [users])
const usersByRole = useMemo(() => {
return users?.reduce((acc, user) => {
if (!acc[user.role]) acc[user.role] = []
acc[user.role].push(user)
return acc
}, {} as Record<string, any>) || {}
}, [users])
return <UserTable sorted={sortedUsers} byRole={usersByRole} />
}
Conditional Query Execution
Control which queries execute based on runtime conditions, permissions, or feature flags, optimizing resource usage and enabling progressive feature rollout.
Permission & Feature-Based Loading
export function Dashboard() {
// Only fetch if admin
const isAdmin = useIsAdmin()
const { data: analytics } = useGetAnalyticsQuery(undefined, {
skip: !isAdmin
})
// A/B testing
const useNewUI = useFeatureFlag('new-ui')
const { data: legacyData } = useGetPostsQuery(undefined, {
skip: useNewUI
})
const { data: modernData } = useGetPostsV2Query(undefined, {
skip: !useNewUI
})
return <Dashboard data={modernData || legacyData} analytics={analytics} />
}
Best Practices & Anti-Patterns
Follow these patterns to build robust, performant applications.
Recommended Patterns
- Use
skipoption for conditional queries - Call
reFetch()to manually refetch when needed - Transform responses for your app's data format
- Use appropriate
cacheTimeoutvalues - Handle errors in component UI
Anti-Patterns to Avoid
- Don't call hooks conditionally (use
skipinstead) - Don't create API instances inside components
- Don't forget to handle loading states
- Don't pass
undefinedas query arguments - Don't ignore error states in UI
Cache Management Utilities
Zustic Query provides powerful utility functions for advanced cache manipulation, enabling optimistic updates, tag-based invalidation, and programmatic cache control.
updateQueryData - Optimistic Updates
Update cached query data programmatically without refetching. Perfect for optimistic updates where you update the cache immediately while mutations are in flight.
/**
* Updates cached query data for a specific endpoint and arguments.
*
* Useful for optimistic updates or manual cache manipulation.
* The updater function receives the current data and should return the updated data.
*/
api.utils.updateQueryData(key: string, arg: any, updater: (data: any) => any): void
Optimistic Update Example
export function UpdateUserEmail() {
const [email, setEmail] = useState('')
const { mutate: updateUser } = useUpdateUserMutation()
const handleSubmit = async () => {
try {
// Optimistically update cache
api.utils.updateQueryData('getUser', { id: 1 }, (draft) => ({
...draft,
email: email
}))
// Then mutate on server
const result = await updateUser({ id: 1, email })
setEmail('')
} catch (error) {
console.error('Failed to update:', error)
// Cache will be refetched on error
}
}
return (
<div>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="New email"
/>
<button onClick={handleSubmit}>Update Email</button>
</div>
)
}
Bulk Cache Operations
Transform entire cached datasets with complex logic:
// Remove user from cached list
api.utils.updateQueryData('getUsers', undefined, (draft) => {
return draft.filter(user => user.id !== userId)
})
// Sort cached users alphabetically
api.utils.updateQueryData('getUsers', undefined, (draft) => {
draft.sort((a, b) => a.name.localeCompare(b.name))
return draft
})
// Add new item to cached list
api.utils.updateQueryData('getUsers', undefined, (draft) => {
draft.push(newUser)
return draft
})
// Map and transform cached data
api.utils.updateQueryData('getUserPosts', { userId: 1 }, (draft) => {
return draft.map(post => ({
...post,
edited: true,
updatedAt: new Date().toISOString()
}))
})
invalidateTags - Tag-Based Cache Invalidation
Invalidate cached queries by tag names. This is essential after mutations when you want to refresh all related data without knowing specific cache keys.
/**
* Invalidates cached queries by tag names.
*
* Clears the cache for all endpoints whose providesTags match the provided tags.
* Supports both simple string tags and object tags with specific IDs.
*/
api.utils.invalidateTags(tags?: (string | {type: string; id?: string | number})[]): void
Invalidate All Related Data After Mutation
export function CreateUserForm() {
const [name, setName] = useState('')
const { mutate: createUser } = useCreateUserMutation()
const handleSubmit = async () => {
try {
const newUser = await createUser({ name })
// Invalidate all 'users' related cache
// This triggers refetch for all queries that provide 'users' tag
api.utils.invalidateTags(['users'])
setName('')
} catch (error) {
console.error('Failed to create user:', error)
}
}
return (
<form onSubmit={(e) => {
e.preventDefault()
handleSubmit()
}}>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="User name"
/>
<button type="submit">Create User</button>
</form>
)
}
Invalidate Specific Items with ID Tags
export function DeleteUserButton({ userId }: { userId: number }) {
const { mutate: deleteUser } = useDeleteUserMutation()
const handleDelete = async () => {
try {
await deleteUser(userId)
// Invalidate the specific user and all their posts
api.utils.invalidateTags([
{ type: 'users', id: userId },
{ type: 'posts', id: userId }
])
} catch (error) {
console.error('Failed to delete user:', error)
}
}
return <button onClick={handleDelete}>Delete User</button>
}
Multiple Tag Invalidation After Complex Mutations
export function TransferOwnershipForm() {
const { mutate: transferOwnership } = useTransferOwnershipMutation()
const handleTransfer = async (postId: number, newOwnerId: number) => {
try {
await transferOwnership({ postId, newOwnerId })
// Invalidate multiple related caches
api.utils.invalidateTags([
{ type: 'posts', id: postId },
{ type: 'userPosts', id: newOwnerId },
'posts' // Refresh all posts
])
} catch (error) {
console.error('Failed to transfer:', error)
}
}
return (
// ... form UI
)
}
resetApiState - Full Cache Reset
Completely reset the API state by clearing all cached data and refetching active queries. Useful for scenarios like user logout or resetting application state.
/**
* Resets the entire API state by clearing the cache for all queries.
*
* This function iterates through all cached queries and clears their cache,
* which effectively resets all cached data. Useful for user logout or app reset.
*/
api.utils.resetApiState(): void
Reset on Logout
export function LogoutButton() {
const handleLogout = async () => {
try {
// Clear auth token from storage
localStorage.removeItem('authToken')
// Reset entire API state
// Clears all sensitive user data from cache
api.utils.resetApiState()
// Redirect to login
window.location.href = '/login'
} catch (error) {
console.error('Logout failed:', error)
}
}
return (
<button onClick={handleLogout} className="logout-btn">
Logout
</button>
)
}
Reset on Permission Change
export function PermissionGuard({ requiredPermission, children }: any) {
const [hasPermission, setHasPermission] = useState(false)
useEffect(() => {
const checkPermission = async () => {
try {
const result = await api.getPermissions()
const allowed = result.permissions.includes(requiredPermission)
if (!allowed) {
// User lost permission - clear all cached data
api.utils.resetApiState()
setHasPermission(false)
return
}
setHasPermission(true)
} catch (error) {
api.utils.resetApiState()
setHasPermission(false)
}
}
checkPermission()
}, [requiredPermission])
return hasPermission ? children : <AccessDenied />
}
refetchQuery - Single Query Refresh
Manually refetch a specific query, bypassing cache and forcing fresh data. Useful for explicit refresh buttons or after certain user actions.
/**
* Clears cache and refetches a specific query by endpoint key and arguments.
*
* Useful when you want to refresh a single query without affecting others.
* Always bypasses cache and fetches fresh data.
*/
api.utils.refetchQuery(key: string, arg: any): void
Manual Refresh Button
export function UsersList() {
const { data: users, isLoading } = useGetUsersQuery()
const handleRefresh = () => {
// Refetch only this specific query
api.utils.refetchQuery('getUsers', undefined)
}
return (
<div>
<button onClick={handleRefresh} disabled={isLoading}>
{isLoading ? 'Refreshing...' : '🔄 Refresh Users'}
</button>
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
Selective Query Refresh
export function Dashboard() {
const { data: stats } = useGetStatsQuery()
const { data: users } = useGetUsersQuery()
const handleStatsRefresh = () => {
// Only refresh stats, don't touch users cache
api.utils.refetchQuery('getStats', undefined)
}
const handleUsersRefresh = () => {
// Only refresh users with specific filter
api.utils.refetchQuery('getUsers', { role: 'admin' })
}
return (
<div>
<section>
<button onClick={handleStatsRefresh}>Refresh Stats</button>
<Stats data={stats} />
</section>
<section>
<button onClick={handleUsersRefresh}>Refresh Admin Users</button>
<UsersList data={users} />
</section>
</div>
)
}
Conditional Auto-Refresh
export function RealTimeStats() {
const { data: stats, isLoading } = useGetStatsQuery()
// Auto-refresh specific query every 30 seconds
useEffect(() => {
const interval = setInterval(() => {
api.utils.refetchQuery('getStats', undefined)
}, 30 * 1000)
return () => clearInterval(interval)
}, [])
return (
<div>
<div>Last updated: {stats?.timestamp}</div>
<Stats data={stats} loading={isLoading} />
</div>
)
}
Combining Multiple Utilities
These utilities work together for powerful cache management patterns:
export function ComplexMutationFlow() {
const { mutate: createPost } = useCreatePostMutation()
const handleCreatePost = async (title: string, content: string) => {
try {
// 1. Optimistically update lists
api.utils.updateQueryData('getPosts', undefined, (draft) => [
...draft,
{ id: 'tmp', title, content, status: 'pending' }
])
// 2. Perform mutation
const post = await createPost({ title, content })
// 3. Clean up optimistic data and refetch
api.utils.refetchQuery('getPosts', undefined)
// 4. Invalidate related caches
api.utils.invalidateTags([
{ type: 'posts', id: post.id },
'postCount'
])
} catch (error) {
// On error, invalidate to get fresh data
api.utils.invalidateTags(['posts'])
}
}
return (
// ... form UI
)
}