Skip to content

createQuery

import { createQuery } from '@effector-tanstack-query/core'
// Uses the default $queryClient (set via setQueryClient / fork values).
function createQuery<TQueryFnData, TError = Error, TData = TQueryFnData>(
options: CreateQueryOptions<TQueryFnData, TError, TData>,
): QueryResult<TData, TError>
// Explicit client — locks the factory to this client; fork({ values })
// overrides of $queryClient do not apply.
function createQuery<TQueryFnData, TError = Error, TData = TQueryFnData>(
queryClient: QueryClient,
options: CreateQueryOptions<TQueryFnData, TError, TData>,
): QueryResult<TData, TError>

Options

CreateQueryOptions extends QueryObserverOptions from @tanstack/query-core, with these adaptations:

FieldTypeNotes
queryKeyEffectorQueryKeyArray; elements may be Store or value
enabledboolean | Store<boolean>Reactive — accepts a store
refetchIntervalnumber | false | ((q) => number | false) | Store<number | false | undefined>Static, function form (TanStack Query), or Store form for runtime polling toggling
namestring (recommended)Stable name for SID-based SSR
…restAll other QueryObserverOptionsstaleTime, gcTime, retry, select, refetchOnMount, refetchOnWindowFocus, refetchOnReconnect, placeholderData, meta, networkMode, …

EffectorQueryKey:

type EffectorQueryKey = ReadonlyArray<
StoreOrValue<string | number | bigint | boolean | null | undefined | object>
>

Cancellation

queryFn receives the standard TanStack AbortSignal as context.signal. Forward it to fetch (or any abortable API) and in-flight requests are cancelled automatically on key change, unmounted(), or a createCancel event — no extra wiring.

const userQuery = createQuery({
name: 'user',
queryKey: ['user', $userId],
queryFn: ({ queryKey, signal }) =>
fetch(`/api/user/${queryKey[1]}`, { signal }).then((r) => r.json()),
})

Return value (QueryResult<TData, TError>)

FieldTypeDescription
$dataStore<TData | undefined>The selected data (post-select)
$errorStore<TError | null>Last error
$statusStore<'pending' | 'success' | 'error'>Query status
$isPendingStore<boolean>No data yet
$isFetchingStore<boolean>Request in flight
$isSuccessStore<boolean>Has successful data
$isErrorStore<boolean>Failed
$isPlaceholderDataStore<boolean>Showing placeholder
$fetchStatusStore<'fetching' | 'paused' | 'idle'>Underlying fetch status
mountedEventCallable<void>Bump reference count; the first mount subscribes the observer
unmountedEventCallable<void>Decrement; the last unmount unsubscribes + cancels inflight
refreshEventCallable<void>Invalidate + refetch
prefetchEventCallable<void>queryClient.fetchQuery + awaits; for SSR / route loaders
$observerStore<QueryObserver<TData, TError> | null>Per-scope observer (created on mounted())
$queryClientStore<QueryClient | null>Resolved client for this query
finished{ success: Event<TData>; failure: Event<TError> }Lifecycle events for sample-driven reactions

Lifecycle events

finished.success and finished.failure let you react to fetch completion from module-level sample wiring — no polling on $status, no manual diffing. They mirror createMutation’s finished.

EventFires when…Payload
finished.successA fetch resolves successfully — fresh fetch, refresh(), reactive key change, or a cross-scope setQueryDataTData (post-select)
finished.failureA fetch failsTError
const userQuery = createQuery({
name: 'user',
queryKey: ['user', $userId],
queryFn: ({ queryKey }) => fetchUser(queryKey[1]),
})
// After every successful fetch, load dependent data.
sample({
clock: userQuery.finished.success,
target: loadSettings,
})
// Toast on every failure.
sample({
clock: userQuery.finished.failure,
fn: (err) => `Failed: ${err.message}`,
target: showToast,
})

The payload is the data / error directly (not { params, result } like a mutation) — a query has no per-call variations, and its key is resolved by the factory. If you need the resolved key alongside the data, add a second sample that reads $status or the internal __resolvedKey store.

Baseline — what does not fire. On the first observation in a scope (e.g. mounted() over SSR-hydrated cache data) neither event fires. The events track new fetches, not the initial observability of already-cached data — otherwise every page load would re-fire success for hydrated data. Each fork scope tracks its own baseline independently. Placeholder data ($isPlaceholderData) never fires success either; only a real resolution does. On the server, prefetch populates the cache without an observer subscription, so no lifecycle events fire there.

prefetch vs mounted

TriggerWhat it doesallSettled returns when…Use case
mountedCreates the Observer, subscribes — initial fetch runs in backgroundThe Observer is set upComponent mount, in-page subscription
prefetchCalls queryClient.fetchQuery and awaits the resultThe query has resolved (data cached)SSR prefetch, route loaders, on-hover prime

A typical SSR flow uses both:

await allSettled(userQuery.prefetch, { scope }) // populates qc cache
await allSettled(userQuery.mounted, { scope }) // dispatches into $data, $status, ...

prefetch is a no-op when enabled is false.

Generic inference

// TQueryFnData inferred from queryFn
const q1 = createQuery({
name: 'q1',
queryKey: ['x'],
queryFn: () => Promise.resolve({ id: 1, name: 'A' }),
})
// q1.$data: Store<{ id: number; name: string } | undefined>
// TData narrowed via select
const q2 = createQuery({
name: 'q2',
queryKey: ['x'],
queryFn: () => Promise.resolve({ id: 1, name: 'A' }),
select: (data) => data.name,
})
// q2.$data: Store<string | undefined>

Custom error type:

class HttpError extends Error { code = 0 }
const q = createQuery<User, HttpError>({ /* ... */ })
// q.$error: Store<HttpError | null>