Skip to content

useQuery

React hook for data fetching that 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.

import { useQuery } from "@usels/integrations";

useQuery accepts DeepMaybeObservable<UseQueryOptions<TData>> — each field can be a plain value or an Observable.

OptionTypeRequiredDescription
queryKeyreadonly unknown[]YesQuery key array. Elements can be plain values, Observables, or plain objects containing Observables.
queryFn() => Promise<TData>YesFunction to fetch data. Use .peek() inside to avoid registering reactive deps.
enabledMaybeObservable<boolean>Whether the query should run. Defaults to true.
staleTimeMaybeObservable<number>Time in ms before data is considered stale.
gcTimeMaybeObservable<number>Time in ms before inactive query cache is garbage collected.
retryMaybeObservable<number | boolean>Number of retry attempts on failure, or false to disable.
refetchOnWindowFocusMaybeObservable<boolean>Refetch when window regains focus.
refetchOnMountMaybeObservable<boolean>Refetch when component mounts.
refetchOnReconnectMaybeObservable<boolean>Refetch when network reconnects.
throwOnErrorboolean | ((error: Error) => boolean)Throw errors to the nearest error boundary.
suspensebooleanEnable React Suspense mode. Requires a <Suspense> boundary in the tree.

Observable<QueryState<TData>> — all fields are observable. Access values with .get() inside reactive contexts or .peek() for non-reactive reads.

FieldTypeDescription
dataTData | undefinedFetched data.
errorError | nullError from the last failed fetch.
status"pending" | "error" | "success"Overall query status.
fetchStatus"fetching" | "paused" | "idle"Current fetch lifecycle status.
isPendingbooleanNo data yet and a fetch is in progress.
isSuccessbooleanData has been fetched successfully.
isErrorbooleanLast fetch resulted in an error.
isLoadingbooleanisPending && isFetching — first fetch in progress.
isFetchingbooleanAny fetch (initial or background) is in progress.
isRefetchingbooleanBackground refetch in progress (data already exists).
isPausedbooleanFetch is paused (e.g. offline).
isStalebooleanData is older than staleTime.
isFetchedbooleanData has been fetched at least once.
isFetchedAfterMountbooleanData was fetched after the current mount.
isEnabledbooleanWhether the query is currently enabled.
isLoadingErrorbooleanError occurred during the initial load.
isRefetchErrorbooleanError occurred during a background refetch.
isPlaceholderDatabooleanCurrently showing placeholder data.
dataUpdatedAtnumberTimestamp of the last successful data update.
errorUpdatedAtnumberTimestamp of the last error.
failureCountnumberNumber of consecutive failures.
failureReasonError | nullReason for the last failure.
errorUpdateCountnumberTotal number of errors encountered.
refetch() => voidManually trigger a refetch.
import {
function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
} from "@usels/integrations";
function
function ProductList(): React.JSX.Element
ProductList
() {
const
const query: any
query
=
useQuery<unknown>(options: DeepMaybeObservable<UseQueryOptions<unknown>>): Observable<QueryState<unknown>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
({
queryKey: {}
queryKey
: ["products"],
queryFn: () => any
queryFn
: () =>
any
fetch
("/api/products").
any
then
((
r: any
r
) =>
r: any
r
.
any
json
()),
});
return (
<
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
{
const query: any
query
.
any
isLoading
.
any
get
() && <
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>Loading...</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>}
{
const query: any
query
.
any
isError
.
any
get
() && <
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>Error: {
const query: any
query
.
any
error
.
any
get
()?.
any
message
}</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>}
{
const query: any
query
.
any
data
.
any
get
()?.
any
map
((
p: any
p
: any) => (
<
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
React.Attributes.key?: React.Key | null | undefined
key
={
p: any
p
.
any
id
}>{
p: any
p
.
any
name
}</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
))}
</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
);
}

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 {
function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
} from "@usels/integrations";
import {
function observable<T>(): Observable<T | undefined> (+2 overloads)
observable
} from "@legendapp/state";
const
const id$: any
id$
=
observable<unknown>(value: Promise<unknown> | (() => unknown) | unknown): any (+2 overloads)
observable
("1");
function
function UserProfile(): React.JSX.Element
UserProfile
() {
const
const user$: any
user$
=
useQuery<unknown>(options: DeepMaybeObservable<UseQueryOptions<unknown>>): Observable<QueryState<unknown>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
({
queryKey: {}
queryKey
: ["users",
const id$: any
id$
], // re-fetches when id$ changes
queryFn: () => any
queryFn
: () =>
any
fetchUser
(
const id$: any
id$
.
any
peek
()),
});
return <
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>{
const user$: any
user$
.
any
data
.
any
get
()?.
any
name
}</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>;
}
// Changing id$ triggers a refetch automatically
const id$: any
id$
.
any
set
("2");

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 {
function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
} from "@usels/integrations";
import {
function observable<T>(): Observable<T | undefined> (+2 overloads)
observable
} from "@legendapp/state";
const
const filter$: any
filter$
=
observable<unknown>(value: Promise<unknown> | (() => unknown) | unknown): any (+2 overloads)
observable
({
category: string
category
: "electronics" });
const
const list$: any
list$
=
useQuery<unknown>(options: DeepMaybeObservable<UseQueryOptions<unknown>>): Observable<QueryState<unknown>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
({
queryKey: {}
queryKey
: ["products", {
filter: any
filter
:
const filter$: any
filter$
.
any
category
}],
queryFn: () => any
queryFn
: () =>
any
fetchProducts
(
const filter$: any
filter$
.
any
category
.
any
peek
()),
});
// Changing filter$.category triggers a refetch
const filter$: any
filter$
.
any
category
.
any
set
("clothing");

Individual options like enabled, staleTime, etc. also accept Observable values.

import {
function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
} from "@usels/integrations";
import {
function observable<T>(): Observable<T | undefined> (+2 overloads)
observable
} from "@legendapp/state";
const
const enabled$: any
enabled$
=
observable<unknown>(value: Promise<unknown> | (() => unknown) | unknown): any (+2 overloads)
observable
(false);
const
const data$: any
data$
=
useQuery<unknown>(options: DeepMaybeObservable<UseQueryOptions<unknown>>): Observable<QueryState<unknown>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
({
queryKey: {}
queryKey
: ["dashboard"],
queryFn: any
queryFn
:
any
fetchDashboard
,
enabled: any
enabled
:
const enabled$: any
enabled$
, // query only runs when enabled$ is true
});
// Enable the query dynamically
const enabled$: any
enabled$
.
any
set
(true);
import {
function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
} from "@usels/integrations";
function
function DataPanel(): React.JSX.Element
DataPanel
() {
const
const query: any
query
=
useQuery<unknown>(options: DeepMaybeObservable<UseQueryOptions<unknown>>): Observable<QueryState<unknown>>

Custom hook that bridges TanStack Query with Legend-State. Manages query state as an observable using QueryObserver.

Accepts DeepMaybeObservable<UseQueryOptions>, 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

// Plain values
const products$ = useQuery({
queryKey: ['products'],
queryFn: () => fetch('/api/products').then(r => r.json())
})
// Observable element in queryKey array
const id$ = observable('1')
const user$ = useQuery({
queryKey: ['users', id$],
queryFn: () => fetchUser(id$.peek()),
})
// Automatically re-fetches when id$ changes.
// Cache is accessible via queryClient.getQueryData(['users', '1'])
// Observable inside a nested object in queryKey
const filter$ = observable({ category: 'electronics' })
const list$ = useQuery({
queryKey: ['products', { filter: filter$.category }],
queryFn: () => fetchProducts(filter$.category.peek()),
})
// Per-field Observable options
const enabled$ = observable(false)
const data$ = useQuery({
queryKey: ['test'],
queryFn: fetchData,
enabled: enabled$,
})

useQuery
({
queryKey: {}
queryKey
: ["stats"],
queryFn: any
queryFn
:
any
fetchStats
,
});
return (
<
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
<
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>Updated: {new
any
Date
(
const query: any
query
.
any
dataUpdatedAt
.
any
get
()).
any
toLocaleTimeString
()}</
React.JSX.IntrinsicElements.p: React.DetailedHTMLProps<React.HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>
p
>
<
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefined
onClick
={() =>
const query: any
query
.
any
refetch
.
any
get
()()}>Refresh</
React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>
button
>
</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
);
}
  • queryFn and .peek() — Always use .peek() (not .get()) inside queryFn when reading observable values. Using .get() would register reactive dependencies and cause unexpected re-renders.
  • Observable state fields — All returned fields are Observable. To read them in a reactive component, call .get(). For non-reactive reads (e.g. event handlers), use .peek().
  • Cache key identity — Observable elements in queryKey are resolved to their plain values before being passed to TanStack Query. The cache key is always a plain array, matching TanStack’s standard behavior.
  • staleTime and caching — When queryKey changes, TanStack decides whether to fetch fresh data or serve from cache based on staleTime. No manual refetch() is needed on key changes.
export interface UseQueryOptions<TData = unknown> {
queryKey: readonly unknown[];
queryFn: () => Promise<TData>;
enabled?: MaybeObservable<boolean>;
staleTime?: MaybeObservable<number>;
gcTime?: MaybeObservable<number>;
retry?: MaybeObservable<number | boolean>;
refetchOnWindowFocus?: MaybeObservable<boolean>;
refetchOnMount?: MaybeObservable<boolean>;
refetchOnReconnect?: MaybeObservable<boolean>;
throwOnError?: boolean | ((error: Error) => boolean);
suspense?: MaybeObservable<boolean>;
}
export interface QueryState<TData = unknown> {
data: TData | undefined;
error: Error | null;
status: "pending" | "error" | "success";
fetchStatus: "fetching" | "paused" | "idle";
isPending: boolean;
isSuccess: boolean;
isError: boolean;
isLoadingError: boolean;
isRefetchError: boolean;
isFetching: boolean;
isPaused: boolean;
isRefetching: boolean;
isLoading: boolean;
isInitialLoading: boolean;
isStale: boolean;
isPlaceholderData: boolean;
isFetched: boolean;
isFetchedAfterMount: boolean;
isEnabled: boolean;
dataUpdatedAt: number;
errorUpdatedAt: number;
failureCount: number;
failureReason: Error | null;
errorUpdateCount: number;
refetch: () => void;
}
export declare function useQuery<TData = unknown>(options: DeepMaybeObservable<UseQueryOptions<TData>>): Observable<QueryState<TData>>;

View on GitHub

  • tigerwest
  • a7392ab 2026-03-06 - feat(core,browser): add sync strategy hooks (tigerwest)