@statekit/decoration-tanstack-query
v0.0.2-dev
Published
query and infiniteQuery decorations for StateKit machines
Maintainers
Readme
@statekit/decoration-tanstack-query
TanStack Query for StateKit machines. A query's data, loading state, and error are written into the machine.
Work in progress.
Install
npm i @statekit/core @statekit/decoration-tanstack-query @tanstack/query-coreRequires @tanstack/query-core 5.104 or later.
Quick start
import { createMachine, createMachineConfig } from "@statekit/core"
import {
setTanstackQueryClient,
withTanstackQuery,
} from "@statekit/decoration-tanstack-query"
import { QueryClient } from "@tanstack/query-core"
setTanstackQueryClient(new QueryClient())
const todoConfig = createMachineConfig({
states: ["idle", "loading", "ready", "failed"],
data: { todoId: 5, todo: null as Todo | null, error: null as unknown },
events: {
open: ({ draft }, id: number) => {
draft.todoId = id
},
},
})
const todoMachine = createMachine(
withTanstackQuery(todoConfig, {
queryFn: ({ data, signal }) => getTodo(data.todoId, signal),
into: (d) => d.todo,
staleTime: 30_000,
})
)
await todoMachine.event.fetch()
todoMachine.data.todo // todo 5Set the client once, before any config is decorated; it's the default client of every query and infinite query. queryFn gets TanStack's context with the machine's data on it, and its result is written into into. The machine and the cache hold the same object, so nothing is copied.
Events
fetch()uses the cache while it's fresh.refetch()always goes to the network.cancel()stops the fetch.subscribe()keeps the machine up to date and returns the unsubscribe.sync()writes what the cache has into the machine.subscribecalls it on each update.
Each takes the arguments queryFn takes after its context, and into, error, hasMore and queryKey get them too. Items fetched by id can live in one record:
const todosMachine = createMachine(
withTanstackQuery(todosConfig, {
queryFn: ({ signal }, id: number) => getTodo(id, signal),
into: (d, id) => d.todos[id],
})
)
await todosMachine.event.fetch(5) // written to d.todos[5]A selector named after the fields into goes through reads what into points at, and fetches it when it's missing or stale. It takes the same arguments:
const todo = todosMachine.select.todos(5) // d.todos[5](d) => d.profile.user is select.profileUser.
The selector fetches, and subscribe writes, while a component uses the machine under StatekitProvider.
It fetches what isn't cached yet, what was invalidated, and what's past a staleTime that's set. Without a staleTime, reading again doesn't fetch again, that's what subscribe is for. Nothing is fetched while it's already being fetched, and a failed query isn't fetched again, that takes refetch.
States and fields
| Option | Defaults to | Set with |
| --------- | -------------------------- | --------------------- |
| loading | loading state | ({ S }) => S.busy |
| success | ready or success state | ({ S }) => S.done |
| failed | failed or error state | ({ S }) => S.broken |
| error | error field | (d) => d.lastError |
When nothing matches, that part is skipped. A failed fetch keeps the data that was already there. The states are the machine's and follow the fetch that reported last, so with items by id, loading can end while another item still loads.
Every other option, like staleTime, gcTime, retry, or meta, goes to TanStack. Retries are off unless retry is set.
Query keys
The key is made from what queryFn reads off data:
queryFn: ({ data }) => getTodo(data.todoId, data.preview)
// ["todo", 5, { preview: true }]It starts with where the query writes, then the ids it read, then the rest, then the event's arguments into doesn't get. fetch(5) above is ["todos", 5]. A top level field named id, todoId, or todo_id is an id. Fields queryFn doesn't read, like a theme, don't change the key. While an id or an argument into gets is null or undefined, nothing is fetched.
To set the key yourself, pass queryKey: (d) => ["todos", d.todoId].
Subscribe
const unsubscribe = todoMachine.event.subscribe()
unsubscribe()Like a mounted useQuery, it fetches when the data is stale, then refetches on focus, on reconnect, after invalidateQueries, and on refetchInterval. It takes the event's arguments: subscribe(5).
The key is the one the data had when subscribe was called. After an event changes a field the key uses, subscribe again.
Infinite queries
import { withTanstackInfiniteQuery } from "@statekit/decoration-tanstack-query"
const feedMachine = createMachine(
withTanstackInfiniteQuery(feedConfig, {
initialPageParam: 0,
queryFn: ({ pageParam }) => getPosts(pageParam),
into: (d) => d.posts,
})
)
await feedMachine.event.fetch()
await feedMachine.event.fetchNextPage()
feedMachine.data.posts // the posts of both pages, in one listThe pages are kept as one flat list, from each page's items. The next page's cursor is the last page's nextCursor. Set items and getNextPageParam when the pages look different.
fetchNextPage loads in the loadingMore state, and hasMore is written to the hasMore field. It loads the first page when there's none, and does nothing after the last one.
