Advanced
Advanced Settings
Section titled “Advanced Settings”dedupe
Section titled “dedupe”Controls request deduplication behavior. There are two deduplication strategies:
- By loading state — prevents new requests if one is already in progress
- By time window — prevents requests within the dedupe time window (queries only, not mutations)
Default: both enabled.
import { client, dedupe } from '@nano_kit/query'
/* Disable time-based deduplication, keep loading state deduplication */const { query } = client( dedupe(true, false))
/* Disable all deduplication for a specific query */const [$post] = query(PostKey, [$postId], fetchPost, [ dedupe(false)])For mutations, only loading state deduplication is available:
const [updatePost] = mutation(updatePostFn, [ dedupe(false) /* Allow concurrent mutation calls */])When to use: Allow multiple simultaneous requests for the same data, such as polling or real-time updates.
disabled
Section titled “disabled”Disables request execution based on a signal value. When the signal returns true, requests are not made.
import { signal, computed } from '@nano_kit/store'import { client, disabled } from '@nano_kit/query'
const $isAuthenticated = signal(false)const $userId = signal(null)
const { query } = client()
/* Disable query when user is not authenticated */const [$user] = query(UserKey, [$userId], fetchUser, [ disabled(computed(() => !$isAuthenticated()))])When to use: Conditionally enable queries based on application state, such as authentication status or availability of required parameters.
mapError
Section titled “mapError”Customizes how errors are converted to strings for storage in cache signals. By default, errors are mapped to error.message.
import { client, mapError } from '@nano_kit/query'
/* Global error mapping */const { query } = client( mapError((err) => { if (err instanceof NetworkError) { return `Network error: ${err.code}` }
return `Error: ${err.message}` }))
/* Per-query error mapping */const [$post] = query(PostKey, [$postId], fetchPost, [ mapError((err) => { if (err.status === 404) { return 'Post not found' }
return 'Failed to load post' })])When to use: Custom error formatting, localization, or extracting specific information from custom error types.
Sets a Codec for query cache entry data when the cache crosses a serialization boundary.
You do not need this for ordinary in-memory browser cache usage. While the app is running in the browser, cache entries can hold any JavaScript value directly. A codec is only needed when query cache data must be serialized: for SSR dehydration/hydration or for persistent storage.
Use it when serialized cache entries contain values that need custom encoding, such as Date, Map, Set, class instances, or encrypted/compact payloads.
hydratable(), ssr(), and persistence() use the codec when they dehydrate data, hydrate it back, write it to persistent storage, or read it back.
import type { Codec } from '@nano_kit/store'import { parse, stringify } from 'devalue'import { client, codec, ssr } from '@nano_kit/query'
const DevalueCodec: Codec<unknown, string> = { encode: stringify, decode: value => value === null ? null : parse(value)}
const { query } = client( codec(DevalueCodec), ssr())With a codec like this, data containing values such as Date, Map, or Set can be dehydrated on the server and restored on the client without losing those types.
The codec applies to cached data. Query metadata such as revision, dedupe time, and expiration time is handled by the query package itself.
When to use: SSR hydration or persistent storage for cache entries that contain non-plain values or require a custom serialized format.
onEveryError
Section titled “onEveryError”Registers a global error handler called for every query or mutation error. The callback receives the error and a stopped boolean indicating if error propagation was stopped.
import { client, onEveryError } from '@nano_kit/query'
const { query } = client( onEveryError((error, stopped) => { if (!stopped) { /* Log to error tracking service */ errorTracker.log(error)
/* Show user notification */ showErrorToast(String(error)) } }))You can prevent the global handler from running using stopErrorPropagation in request context:
import { onError, stopErrorPropagation } from '@nano_kit/query'
const [$post] = query(PostKey, [$postId], (id, ctx) => { /* Stop error conditionally */ onError(ctx, (error) => { if (error.message.includes('404')) { /* Handle 404 silently */ stopErrorPropagation(ctx) } }) /* Or stop all errors */ stopErrorPropagation(ctx)
return fetchPost(id)})When to use: Centralized error handling, logging, or user notifications. Combine with stopErrorPropagation to handle specific errors locally without triggering global handlers.
Request Context
Section titled “Request Context”Query and mutation fetcher functions receive request context as last parameter. It provides methods to manage request lifecycle:
onSuccess(ctx, fn)- register success callbackonError(ctx, fn)- register error callbackonSettled(ctx, fn)- register settled callbackstopErrorPropagation(ctx)- stop error propagation to globalonEveryErrorhandler
import { client, mutations, onSuccess, onError, onSettled, stopErrorPropagation } from '@nano_kit/query'
const { mutation } = client(mutations())/* ... */const [updatePost] = mutation<[params: UpdatePostParams], Post>( (params, ctx) => { onSuccess(ctx, (data) => { /* Data updated successfully */ }) onError(ctx, (error) => { /* Handle error */ }) onSettled(ctx, (data, error) => { /* Always executed */ }) /* Mark error as stopped to prevent global error handling */ /* Can be invoked in onError callback as well */ stopErrorPropagation(ctx)
return PostsService.update(postId, params) })Also query’s context can be used as a cache key for advanced scenarios:
import { client, queryKey } from '@nano_kit/query'
const TagsKey = queryKey<[postId: number], Tag[] | null>('tags')const $postId = signal(1)const { query, $data} = client()const [$tags, $tagsError] = query(TagsKey, [$postId], async (postId, ctx) => { /* Use ctx as current query key to accumulate data in the cache */ const prevTags = $data(ctx) || [] const newTags = await PostsService.fetchTags(postId)
return [...new Set(prevTags.concat(newTags))]})Operations
Section titled “Operations”Operations are a special type of reactive primitive that combines features of queries and mutations. They are essentially “manual queries” — they don’t fetch automatically on mount or parameter changes, but they maintain state in the cache like queries.
Use operation() from the client with the operations() extension. Like queries, you need to define an operationKey:
import { client, operations, operationKey } from '@nano_kit/query'
/* Define operation key */const GenerateKey = operationKey<[], [prompt: string], GeneratedPost>('generate')
const { operation } = client( operations())
/* Create operation */const [generate, $result, $error, $loading] = operation(GenerateKey, [], (prompt) => PostsService.generateWithAi(prompt))Operations are executed manually like mutations, but their result is stored in the cache identified by the key constructed from arguments:
/* Execute operation manually */const [result, error] = await generate('Write a post about React')When to use what?
Section titled “When to use what?”| Feature | Query | Mutation | Operation |
|---|---|---|---|
| Execution | Automatic (reactive) | Manual | Manual |
| Caching | Yes (all calls) | No (result only) | Yes (all calls) |
| Deduplication | Yes | Loading only | Yes |
| Use Case | Fetching data for UI | Modifying data on server | ”Heavy” computations, on-demand fetching with cache |
Use operations for:
- Expensive calculations that should be cached but triggered manually (e.g., AI generation, reports).
- Lazy loading data that shouldn’t be loaded immediately on mount.
- Search actions where you want to cache results for specific search terms but trigger the search manually (e.g., on button click submit).
Infinite
Section titled “Infinite”Infinite queries enable pagination and “Load More” patterns by maintaining an array of pages in the cache.
Use infinite() from infinites() extension. It requires a getNextCursor function to determine the next page’s cursor.
import { type InfinitePages, client, infinites, queryKey } from '@nano_kit/query'
interface PostsPage { posts: Post[] nextCursor?: number}
/* 1. Define key with InfinitePages<Data, Cursor> type */const PostsKey = queryKey<[], InfinitePages<PostsPage, number>>('posts')
const { infinite } = client(infinites())
/* 2. Create infinite query */const [fetchNext, $data, $error, $loading] = infinite( PostsKey, [], (lastPage) => lastPage.nextCursor, /* Extract next cursor from page */ (cursor) => fetchPosts({ cursor }) /* Fetcher receives cursor */)Returns a tuple with:
fetchNext: Function to load the next page.$data: Signal withInfinitePagesobject:pages: Array of all loaded pagesP[].next: The next cursor valueC.more: Boolean indicating ifnextcursor is present.
$error: Signal with error message (ornull).$loading: Signal indicating loading state.$key: Signal with current cache key.
Here is a small usage example in a React component:
import { useSignal } from '@nano_kit/react'
export function PostsList() { const data = useSignal($data) const loading = useSignal($loading)
if (!data) { return null }
/* Access all items from all pages */ const allPosts = data.pages.flatMap(page => page.posts)
return ( <div> {allPosts.map(post => ( <PostCard key={post.id} post={post} /> ))}
{data.more && ( <button onClick={fetchNext} disabled={loading}> {loading ? 'Loading...' : 'Load More'} </button> )} </div> )}Extensions
Section titled “Extensions”Extensions allow you to add extra functionality to the client or individual queries.
retryOnError
Section titled “retryOnError”Enables automatic retries when a request fails. It uses an exponential backoff strategy by default to determine the delay between attempts.
import { client, retryOnError } from '@nano_kit/query'
/* Enable retries globally */const { query } = client( retryOnError())
/* Enable retries for a specific query */const [$post] = query(PostKey, [$postId], fetchPost, [ retryOnError()])By default it uses a jittered exponential backoff (starts around 2s, increases up to ~30s). You can provide a custom delay calculator:
retryOnError((count, error) => { /* Linear backoff: 1s, 2s, 3s... */ return count * 1000})When to use: Improve resilience for unstable network connections or transient server errors.
abortable
Section titled “abortable”Adds support for request cancellation using AbortController. It injects an AbortSignal into the request context, which you can pass to fetch or other async APIs.
import { client, abortable, abortSignal, abortPrevious, abort } from '@nano_kit/query'
const { query } = client( abortable())
const [$post] = query(PostKey, [$postId], async (id, ctx) => { /* Abort previous running request for this query */ abortPrevious(ctx)
/* Pass signal to fetch */ return fetch(`/api/posts/${id}`, { signal: abortSignal(ctx) }).then(r => r.json())})You can also manually abort a running request:
const promise = fetchPost()
abort(promise)When to use: Data fetching where cancellation prevents race conditions and saves bandwidth when the user navigates away or parameters change quickly (e.g., search-as-you-type).
revalidateOn
Section titled “revalidateOn”Revalidates queries when one or more reactive conditions become truthy. Pass any accessor that represents the moment when cached data should be refreshed: page visibility, network state, polling ticks, or your own domain-specific condition.
import { interval } from '@nano_kit/store'import { $networkOnline, $pageVisible} from '@nano_kit/platform-web'import { client, revalidateOn } from '@nano_kit/query'
const { query } = client( revalidateOn( /* Revalidate when page becomes visible again... */ $pageVisible, /* ...or when network comes back online... */ $networkOnline, /* ...or every minute */ interval(60 * 1000) ))Use built-in browser signals from @nano_kit/platform-web for common app lifecycle events:
$pageVisiblerevalidates when the user returns to the page.$networkOnlinerevalidates when the browser comes back online.interval(ms)from@nano_kit/storerevalidates on a timer.
You can also pass your own signal or computed condition:
import { computed, signal } from '@nano_kit/store'import { client, revalidateOn } from '@nano_kit/query'
const $userId = signal<string | null>(null)const $ready = computed(() => $userId() !== null)
const { query } = client( revalidateOn($ready))When to use: Keep data fresh when the page becomes visible again, recover after network outages, poll server state, or revalidate from any domain-specific condition.
persistence
Section titled “persistence”Persists the query cache using any storage adapter that implements the Storage interface. The second argument is the data lifetime in milliseconds.
If the client has a codec setting, persisted entry data is encoded before writing and decoded after reading.
import { client, persistence, indexedDbStorage } from '@nano_kit/query'
const { query } = client( /* Keep cache for 24 hours */ persistence(indexedDbStorage(), 24 * 60 * 60 * 1000))indexedDbStorage
Section titled “indexedDbStorage”Creates an IndexedDB storage adapter for persistence(), allowing cached data to survive page reloads and act as a cache for offline mode.
It stores entries prepared by persistence(), including data encoded by the current codec setting.
import { client, persistence, indexedDbStorage } from '@nano_kit/query'
const { query } = client( /* Keep cache for 24 hours */ persistence(indexedDbStorage(), 24 * 60 * 60 * 1000))When to use: Offline-first applications, or faster startup by showing cached data immediately while fetching fresh data.
entities
Section titled “entities”The entities extension allows you to map query or mutation results to entity references for better cache management and data consistency. Thus you can update entity data in one place and have it reflected across all queries that reference that entity.
import { client, mutations, entity, entities, onError } from '@nano_kit/query'
const PostEntity = entity<Post>('post')
const { query, mutation, $data } = client( mutations())
/* 1. Map individual entity in query */const [$post] = query(PostKey, [$postId], (postId) => ( PostsService.fetch(postId)), [ /* Map entity to entity reference */ /* Also every refetch will update entity in the cache */ entities(PostEntity)])
/* 2. Map list of entities in query */const [$posts] = query(PostsKey, [], () => ( PostsService.fetchPosts()), [ /* Map entities in the page to entity references */ /* Also every refetch will update entities in the cache */ entities(page => ({ ...page, posts: page.posts.map(PostEntity) }))])
/* 3. Optimistic update via mutation */const [updatePost] = mutation<[params: UpdatePostParams], Post>( (params, ctx) => { const postId = $postId() /* Get entity key by id */ const postEntityKey = PostEntity(postId) /* Get current entity data */ const post = $data(postEntityKey)
if (post) { /* Optimistically update entity data, will update all references */ $data(postEntityKey, { ...post, ...params })
/* Revert changes on error */ onError(ctx, () => { $data(postEntityKey, post) }) }
return PostsService.update(postId, params) })With this setup:
$post(individual post) and$posts(list of posts) share the same data source for the post entities.- Fetching specific post via
$postupdates the entity in$postsas well. updatePostoptimistically updates the entity, instantly reflecting changes in both$postand$posts.
When to use: Complex applications where the same data (e.g., a “User” or “Product”) appears in multiple places or lists and needs to stay synchronized.
Integrates with @nano_kit/store’s task tracking system. This is mainly used for server-side rendering (SSR) to wait for all data fetches to complete before rendering the HTML.
import { tasksRunner, waitTasks } from '@nano_kit/store'import { client, tasks } from '@nano_kit/query'
const tasksPool = new Set()const runTask = tasksRunner(tasksPool)
const { query } = client( tasks(runTask))
/* ... application runs ... */
/* Wait for all queries to finish */await waitTasks(tasksPool)Without arguments, tasks() reads TasksRunner$ from the current injection context. Outside DI, pass the runner explicitly.
When to use: SSR setups where the server should send a fully populated page to the client.