Skip to content

Queries

A query is created with createQuery(options) (the registered default QueryClient is used) or createQuery(queryClient, options) (explicit client). It returns an object of effector stores and events.

Reactive query keys

Anywhere in queryKey, you can use a Store instead of a plain value. The query refetches automatically when any store updates.

const $userId = createStore(1)
const userQuery = createQuery({
name: 'user',
queryKey: ['user', $userId],
queryFn: ({ queryKey }) => fetchUser(queryKey[1] as number),
})

Mixing stores and primitives is supported:

queryKey: ['posts', 42, $section, { sort: 'asc' }]

Enabled flag

enabled controls whether the query runs. It accepts a boolean OR a Store<boolean>:

// Static
const userQuery = createQuery({
name: 'user',
queryKey: ['user'],
queryFn: fetchUser,
enabled: false, // never fetches until enabled changes
})
// Reactive
const $isLoggedIn = createStore(false)
const profileQuery = createQuery({
name: 'profile',
queryKey: ['profile'],
queryFn: fetchProfile,
enabled: $isLoggedIn, // fetches once $isLoggedIn becomes true
})

Dependent queries

Use one query’s $isSuccess as another’s enabled:

const userQuery = createQuery({
name: 'user',
queryKey: ['user'],
queryFn: fetchUser,
})
const postsQuery = createQuery({
name: 'user-posts',
queryKey: ['posts'],
queryFn: fetchPosts,
enabled: userQuery.$isSuccess,
})

select — narrow data shape

select runs after the queryFn and narrows the displayed TData type:

const userQuery = createQuery({
name: 'user',
queryKey: ['user'],
queryFn: () => fetchUser(), // returns { id, name, email, role }
select: (data) => data.name, // $data: Store<string | undefined>
})

If select throws, the query transitions to error state with the thrown value.

placeholderData

Show data while the new key is loading:

import { keepPreviousData } from '@tanstack/query-core'
const todosQuery = createQuery({
name: 'todos',
queryKey: ['todos', $page],
queryFn: ({ queryKey }) => fetchTodos(queryKey[1]),
placeholderData: keepPreviousData,
})
todosQuery.$isPlaceholderData // Store<boolean>

A static value or function is also supported:

placeholderData: { id: 0, name: 'Loading…' }
// Or a function that gets prevData and prevQuery
placeholderData: (prev) => prev,

Polling with refetchInterval

const statusQuery = createQuery({
name: 'status',
queryKey: ['status'],
queryFn: fetchStatus,
refetchInterval: 5000, // every 5 s
})

A function form lets you stop polling based on data:

refetchInterval: (q) => {
const v = q.state.data as { done: boolean } | undefined
return v?.done ? false : 1000
},

Reactive refetchInterval

refetchInterval also accepts a Store<number | false>. Toggling the store starts / stops polling at runtime — the library calls observer.setOptions({ refetchInterval }) on every store change, so the live observer picks up the new interval immediately.

import { createEvent, createStore } from 'effector'
const togglePolling = createEvent()
const $interval = createStore<number | false>(3000).on(
togglePolling,
(v) => (v === false ? 3000 : false),
)
const statusQuery = createQuery({
queryKey: ['status'],
queryFn: fetchStatus,
refetchInterval: $interval, // ← reactive
})
// Anywhere in your app:
togglePolling() // → stops / resumes polling

This works under fork({ values }) too — each scope drives its own observer through the same store.

refetchOnMount / refetchOnWindowFocus / refetchOnReconnect

All three accept boolean | 'always' | (query) => boolean | 'always' and behave exactly as in TanStack Query. Defaults: true for mount/focus/reconnect.

createQuery({
name: 'auth',
queryKey: ['auth'],
queryFn: fetchAuth,
refetchOnWindowFocus: 'always', // refetch even on fresh data
refetchOnReconnect: false, // never refetch on reconnect
})

Manual refresh

userQuery.refresh() // invalidates the query and refetches in the background

Reacting to fetch completion

finished.success / finished.failure are events you can drive sample from — react to every completed fetch without watching $status by hand.

const userQuery = createQuery({
name: 'user',
queryKey: ['user', $userId],
queryFn: ({ queryKey }) => fetchUser(queryKey[1]),
})
// Chain a dependent load off each successful fetch.
sample({
clock: userQuery.finished.success,
target: loadSettings,
})
// Surface every failure.
sample({
clock: userQuery.finished.failure,
fn: (err) => `Failed: ${err.message}`,
target: showToast,
})

finished.success carries the post-select data; finished.failure carries the error. They fire on fresh fetches, refresh(), and reactive key changes — but not for the baseline state seen on mount (e.g. SSR-hydrated cache). See the createQuery lifecycle events reference for the full semantics.

Lifecycle

You must call mounted() (or use useQuery(query) in React) for the observer to subscribe. unmounted() tears it down.

The observer is shared per Scope and reference-counted: every mounted() is one owner, and only the last matching unmounted() releases the observer. Several components and a feature-level sample can drive the same query independently — unmounting one of them doesn’t stop updates for the rest. Extra unmounted() calls are a safe no-op.

userQuery.mounted()
// ...
userQuery.unmounted() // last owner: cancels in-flight, releases observer

In React, the useQuery hook calls these for you.