Query families
createQueries({ source, query }) builds a family — one parallel query per element of a reactive source store. Adding items spawns observers; removing items disposes them; reordering preserves them. The whole family exposes a single set of effector stores ($items, $data, $isPending, …) plus lifecycle events.
When to use it
| Situation | Use |
|---|---|
One query with reactive parameters (['user', $currentId]) | createQuery |
| A fixed set of unrelated queries on a page (user + posts + settings) | useQueries([f1, f2, f3]) |
| Dynamic list — N queries with the same shape, varying by item | createQueries + useQueries(family) |
Defining a family
import { createStore, createEvent } from 'effector'import { createQueries } from '@effector-tanstack-query/core'
const $userIds = createStore<number[]>([])export const userIdsSet = createEvent<number[]>()$userIds.on(userIdsSet, (_, ids) => ids)
export const usersFamily = createQueries({ name: 'users.list', source: $userIds, query: (id) => ({ queryKey: ['user', id], queryFn: () => fetchUser(id), // `enabled` is a plain boolean — re-runs whenever source changes enabled: id > 0, }), // Shared options applied on top of every per-item `query(id)` staleTime: 60_000,})query(item) must be pure — the same item in always yields the same options. Reactivity comes from the source store; whenever it updates, the callback fires for every item to compute fresh options.
Consuming the family
React, non-suspense
import { useQueries } from '@effector-tanstack-query/react'
function UserList() { const items = useQueries(usersFamily) return items.map((it, i) => it.isPending ? <Skeleton key={i} /> : <UserCard key={i} user={it.data!} />, )}Each item is the standard UseQueryResult shape. Calling items[i].refresh() routes to family.refreshOne(items[i].source) — re-fetches just that one.
React, Suspense
import { useSuspenseQueries } from '@effector-tanstack-query/react'
function UserList() { const items = useSuspenseQueries(usersFamily) // each items[i].data is User (non-nullable past the Suspense gate) return items.map((it) => <UserCard key={it.data.id} user={it.data} />)}Throws an aggregated Promise.all(...) of every pending item — <Suspense> waits until they all settle. First errored item throws to the nearest <ErrorBoundary>.
Pure effector
import { useUnit } from 'effector-react'
function UserList() { const items = useUnit(usersFamily.$items) React.useEffect(() => { usersFamily.mounted() return () => usersFamily.unmounted() }, []) return items.map(/* … */)}mounted() / unmounted() are reference-counted — multiple consumers in the same scope share observers; the last unmount tears them down.
SSR
Plugs into prefetchQueries like any other factory:
import { dehydrate } from '@tanstack/query-core'import { serialize, allSettled } from 'effector'import { prefetchQueries } from '@effector-tanstack-query/core'
// Server-side page handlerconst { queryClient, scope } = makeRequestScope()
await allSettled(userIdsSet, { params: [1, 2, 3], scope })await prefetchQueries([usersFamily], { scope })
return { dehydrated: dehydrate(queryClient), serialized: serialize(scope),}prefetch walks the current source, calls qc.fetchQuery(...) for every item in parallel, awaits all, then mounts observers so $items carries the populated snapshot through serialize(scope). Set the source value via allSettled(sourceEvent, { params: …, scope }) BEFORE prefetch — query(item) reads it synchronously.
Pitfalls
querymust be pure. Don’t readDate.now()or scope state inside it. Reactivity flows throughsource; arbitrary side effects break the diff (same item produces differentqueryKey→ new observer every render).- Duplicates in source. Two equal items produce one observer but two
$itemsentries (parallel to source). Intentional in most cases (rendering the same row twice), occasionally surprising. De-dup at the source level if it matters. - Reordering is cheap. Adding
[1, 2, 3] → [3, 2, 1]doesn’t re-fetch — observers stay keyed byhashKey(queryKey).$itemsjust re-projects. - Per-item
enabledis a plainboolean, not aStore. To switch enabledness, update thesourcesoquery(item).enabledevaluates differently — that’s the only reactive path. nameis required for SSR. Without it the$itemssnapshot is silently dropped fromserialize(scope).
Working example
examples/ssr doesn’t ship a family demo (yet); see the test suite at packages/core/src/__tests__/createQueries.test.ts for end-to-end SSR + diff + lifecycle behaviour.