useQuery
Bridges TanStack Query with Legend-State. Returns query state as an Observable, and accepts Observable values anywhere in the options — including individual elements inside queryKey. When an observable value changes, the query automatically re-fetches. Options follow TanStack Query’s standard UseQueryOptions — refer to TanStack Query docs for full option details. Each option field accepts MaybeObservable<T> for reactive control.
Both variants require a QueryClientProvider ancestor — the client is injected from context.
Select a category — the query auto-refetches via Observable queryKey.
Basic query
Section titled “Basic query”import { function For<T, TProps>({ each, optimized: isOptimized, item, itemProps, sortValues, children, }: { each?: ObservableParam<T[] | Record<any, T> | Map<any, T>>; optimized?: boolean; item?: FC<ForItemProps<T, TProps>>; itemProps?: TProps; sortValues?: (A: T, B: T, AKey: string, BKey: string) => number; children?: (value: Observable<T>, id: string | undefined) => ReactElement;}): ReactElement | null (+1 overload)
For, function Show<T>(props: Props<T>): ReactElement (+2 overloads)
Show, function useObservable<T>(): Observable<T | undefined> (+3 overloads)
A React hook that creates a new observable
useObservable } from "@usels/core";import { const useQuery: <TData = unknown>(options?: DeepMaybeObservable<CreateQueryOptions<TData>>) => Observable<QueryState<TData>>
useQuery } from "@usels/tanstack-query";
function function ProductList(): JSX.Element
ProductList() { const const query: any
query = useQuery<unknown>(options?: DeepMaybeObservable<CreateQueryOptions<unknown>>): Observable<QueryState<unknown>>
Core observable function for bridging TanStack Query with Legend-State.
Must be called inside a scope (e.g. useScope factory or standalone createScope)
wrapped by a QueryClientProvider — getQueryClient() injects the client from
context. Reactive teardown is registered via onUnmount; option changes
(including Observable elements inside queryKey) are tracked via observe.
useQuery({ queryKey: {}
queryKey: ["products"], queryFn: () => any
queryFn: () => any
fetch("/api/products").any
then((r: any
r) => r: any
r.any
json()), });
const const products$: any
products$ = useObservable<unknown>(value: Promise<unknown> | (() => unknown) | unknown, deps?: DependencyList): any (+3 overloads)
A React hook that creates a new observable
useObservable(() => const query: any
query.any
data.any
get() ?? []);
return ( <JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div> <function Show<T>(props: Props<T>): ReactElement (+2 overloads)
Show if: any
if={const query: any
query.any
isLoading}> <JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p>Loading...</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p> </function Show<T>(props: Props<T>): ReactElement (+2 overloads)
Show> <function Show<T>(props: Props<T>): ReactElement (+2 overloads)
Show if: any
if={const query: any
query.any
isError}> <JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p>Error: {const query: any
query.any
error.any
get()?.any
message}</JSX.IntrinsicElements.p: DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p> </function Show<T>(props: Props<T>): ReactElement (+2 overloads)
Show> <function For<T, TProps>({ each, optimized: isOptimized, item, itemProps, sortValues, children, }: { each?: ObservableParam<T[] | Record<any, T> | Map<any, T>>; optimized?: boolean; item?: FC<ForItemProps<T, TProps>>; itemProps?: TProps; sortValues?: (A: T, B: T, AKey: string, BKey: string) => number; children?: (value: Observable<T>, id: string | undefined) => ReactElement;}): ReactElement | null (+1 overload)
For each?: any
each={const products$: any
products$}>{(p$: any
p$) => <JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div>{p$: any
p$.any
name.any
get()}</JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div>}</function For<T, TProps>({ each, optimized: isOptimized, item, itemProps, sortValues, children, }: { each?: ObservableParam<T[] | Record<any, T> | Map<any, T>>; optimized?: boolean; item?: FC<ForItemProps<T, TProps>>; itemProps?: TProps; sortValues?: (A: T, B: T, AKey: string, BKey: string) => number; children?: (value: Observable<T>, id: string | undefined) => ReactElement;}): ReactElement | null (+1 overload)
For> </JSX.IntrinsicElements.div: DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div> );}import { For, observable, Show } from "@usels/core";import { createQuery } from "@usels/tanstack-query";
function ProductList() { "use scope"; const state$ = createQuery({ queryKey: ["products"], queryFn: () => fetch("/api/products").then((r) => r.json()), });
const products$ = observable(() => state$.data.get() ?? []);
return ( <div> <Show if={state$.isLoading}> <p>Loading...</p> </Show> <Show if={state$.isError}> <p>Error: {state$.error.get()?.message}</p> </Show> <For each={products$}>{(p$) => <div>{p$.name.get()}</div>}</For> </div> );}Observable in queryKey (auto-refetch)
Section titled “Observable in queryKey (auto-refetch)”When an element inside queryKey is an Observable, the query automatically re-fetches whenever its value changes. Use .peek() in queryFn to read the current value without registering an extra reactive dependency.
import { useQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const id$ = observable("1");
function UserProfile() { const user$ = useQuery({ queryKey: ["users", id$], queryFn: () => fetchUser(id$.peek()), }); return <p>{user$.data.get()?.name}</p>;}import { createQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const id$ = observable("1");
function UserProfile() { "use scope"; const state$ = createQuery({ queryKey: ["users", id$], queryFn: () => fetchUser(id$.peek()), }); return <p>{state$.data.get()?.name}</p>;}The resolved queryKey is a plain array (e.g. ['users', '1']), so cache lookups via queryClient.getQueryData(['users', '1']) work as expected.
Observable inside a nested object in queryKey
Section titled “Observable inside a nested object in queryKey”Observable values nested inside plain objects within queryKey are also resolved reactively.
import { useQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const filter$ = observable({ category: "electronics" });
function ProductList() { const list$ = useQuery({ queryKey: ["products", { filter: filter$.category }], queryFn: () => fetchProducts(filter$.category.peek()), }); return <div>{list$.data.get()?.length} items</div>;}import { createQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const filter$ = observable({ category: "electronics" });
function ProductList() { "use scope"; const state$ = createQuery({ queryKey: ["products", { filter: filter$.category }], queryFn: () => fetchProducts(filter$.category.peek()), }); return <div>{state$.data.get()?.length} items</div>;}Per-field Observable options
Section titled “Per-field Observable options”Individual options like enabled, staleTime, etc. also accept Observable values.
import { useQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const enabled$ = observable(false);
function Dashboard() { const data$ = useQuery({ queryKey: ["dashboard"], queryFn: fetchDashboard, enabled: enabled$, }); return <div>{data$.status.get()}</div>;}import { createQuery } from "@usels/tanstack-query";import { observable } from "@usels/core";
const enabled$ = observable(false);
function Dashboard() { "use scope"; const state$ = createQuery({ queryKey: ["dashboard"], queryFn: fetchDashboard, enabled: enabled$, }); return <div>{state$.status.get()}</div>;}Manual refetch
Section titled “Manual refetch”import { useQuery } from "@usels/tanstack-query";
function DataPanel() { const query = useQuery({ queryKey: ["stats"], queryFn: fetchStats }); return ( <div> <p>Updated: {new Date(query.dataUpdatedAt.get()).toLocaleTimeString()}</p> <button onClick={() => query.refetch()}>Refresh</button> </div> );}import { createQuery } from "@usels/tanstack-query";
function DataPanel() { "use scope"; const state$ = createQuery({ queryKey: ["stats"], queryFn: fetchStats }); return ( <div> <p>Updated: {new Date(state$.dataUpdatedAt.get()).toLocaleTimeString()}</p> <button onClick={() => state$.refetch.get()()}>Refresh</button> </div> );}export type { CreateQueryOptions, QueryState } from "./core";export { createQuery } from "./core";/** * UseQueryOptions is an alias for CreateQueryOptions for backward compatibility. */export type UseQueryOptions<TData = unknown> = CreateQueryOptions<TData>;/** * Custom hook that bridges TanStack Query with Legend-State. * Manages query state as an observable using QueryObserver. * * Accepts `DeepMaybeObservable<CreateQueryOptions>`, supporting an Observable * for the entire options object or for individual fields. Elements inside * the queryKey array can also be Observables and will react to changes automatically. * * @example * ```tsx * const products$ = useQuery({ * queryKey: ['products'], * queryFn: () => fetch('/api/products').then(r => r.json()) * }) * ``` */export type UseQuery = typeof createQuery;export declare const useQuery: UseQuery;