Data fetching — useQuery + useMutation
The pattern
Pass query options as a function (not an object) so signal reads inside track automatically:
import { useQuery, useMutation, QueryClientProvider, QueryClient } from '@pyreon/query'
import { signal } from '@pyreon/reactivity'
const queryClient = new QueryClient()
function Root() {
return (
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
)
}
function UserCard() {
const userId = signal('alice')
// Options function — signal reads track, so changing userId refetches.
const user = useQuery(() => ({
queryKey: ['user', userId()],
queryFn: () => api.fetchUser(userId()),
staleTime: 30_000,
}))
return (
<div>
{() => user.isLoading() && <Spinner />}
{() => user.data() && <h1>{user.data()!.name}</h1>}
<button onClick={() => userId.set('bob')}>Load Bob</button>
</div>
)
}Mutations use an object (mutations are imperative — no tracking needed) + invalidates to clear matching queries on success:
function DeleteButton(props: { id: string }) {
const deleteUser = useMutation({
mutationFn: (id: string) => api.deleteUser(id),
invalidates: [['users']], // all queries with this prefix invalidate on success
})
return (
<button
disabled={() => deleteUser.isPending()}
onClick={() => deleteUser.mutate(props.id)}
>
Delete
</button>
)
}Key differences from TanStack Query
useQuery/useInfiniteQuery/useQueries/useSuspenseQuerytake options as a function — changing a tracked signal re-runs the observer options and refetches.useMutationoptions are a plain object — mutations fire imperatively via.mutate(...), so there's nothing to track.Every query field is a fine-grained
Signal<T>— readinguser.data()does not subscribe touser.error()oruser.isFetching().useMutation({ invalidates: [...] })auto-invalidates keys on success, preserving useronSuccess.defineQueries({ user: () => opts, posts: () => opts })— named parallel queries returning a typed object, cleaner than the array form.
Why
Pyreon components run once, so a plain options object would bake the initial values in forever. Wrapping in a function lets Pyreon's reactivity system re-invoke the options and drive the TanStack Query observer on each change — the TanStack primitive stays pure, the adapter handles tracking.
Anti-pattern
// BROKEN — options object evaluated ONCE at component setup,
// user.data() never updates even though userId changes.
const user = useQuery({
queryKey: ['user', userId()], // read at setup, captures 'alice' forever
queryFn: () => api.fetchUser(userId()),
})// BROKEN — mutate() fires immediately on render (the call happens
// during prop evaluation; the handler prop gets the return value,
// not a function)
const Broken = () => <button onClick={deleteUser.mutate(id)}>Delete</button>
// Correct — wrap in an arrow so onClick receives a callable
const Correct = () => <button onClick={() => deleteUser.mutate(id)}>Delete</button>Related
Reference API:
useQuery,useMutation,useSubscription,useSSE—get_api({ package: "query", symbol: "..." })Related: WebSocket subscription via
useSubscription({ url, enabled })uses the same options-as-function pattern