snapshot of https://github.com/TanStack/query.git (main, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
111
docs/framework/lit/guides/reactive-controllers-vs-hooks.md
Normal file
111
docs/framework/lit/guides/reactive-controllers-vs-hooks.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
id: reactive-controllers-vs-hooks
|
||||
title: Reactive Controllers vs Hooks
|
||||
---
|
||||
|
||||
React Query examples use hooks. Lit Query uses reactive controllers.
|
||||
|
||||
The job is similar: subscribe a component to a `QueryClient`, read the latest result, and update the component when the cache changes. The integration point is different because Lit components use the `ReactiveControllerHost` interface instead of React's render and hook system.
|
||||
|
||||
## Mapping the Concepts
|
||||
|
||||
| React Query | Lit Query |
|
||||
| --------------------------- | ---------------------------------------------- |
|
||||
| `useQuery(options)` | `createQueryController(this, options)` |
|
||||
| `useQueries(options)` | `createQueriesController(this, options)` |
|
||||
| `useMutation(options)` | `createMutationController(this, options)` |
|
||||
| `useInfiniteQuery(options)` | `createInfiniteQueryController(this, options)` |
|
||||
| `useIsFetching(options)` | `useIsFetching(this, options)` |
|
||||
| `useIsMutating(options)` | `useIsMutating(this, options)` |
|
||||
| `useMutationState(options)` | `useMutationState(this, options)` |
|
||||
| Hook result object | Callable result accessor |
|
||||
| React context provider | `QueryClientProvider` custom element |
|
||||
| Component render rerun | `host.requestUpdate()` |
|
||||
|
||||
## Host-Bound APIs
|
||||
|
||||
Lit APIs that subscribe a component to query or mutation state receive a `host` as the first argument:
|
||||
|
||||
```ts
|
||||
class TodosView extends LitElement {
|
||||
private readonly todos = createQueryController(this, {
|
||||
queryKey: ['todos'],
|
||||
queryFn: fetchTodos,
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
`this` is valid because `LitElement` implements `ReactiveControllerHost`. The controller attaches to the host, subscribes when the host connects, requests updates when the query result changes, and unsubscribes when the host disconnects.
|
||||
|
||||
The host-bound APIs are [`createQueryController`](../reference/functions/createQueryController.md), [`createQueriesController`](../reference/functions/createQueriesController.md), [`createInfiniteQueryController`](../reference/functions/createInfiniteQueryController.md), [`createMutationController`](../reference/functions/createMutationController.md), [`useIsFetching`](../reference/functions/useIsFetching.md), [`useIsMutating`](../reference/functions/useIsMutating.md), and [`useMutationState`](../reference/functions/useMutationState.md).
|
||||
|
||||
[`useQueryClient`](../reference/functions/useQueryClient.md) is different. It is not a reactive controller, does not accept a host, does not subscribe, and throws synchronously if no single default client is available. Use it only for imperative code that runs while exactly one `QueryClientProvider` is connected. Inside host-bound APIs, prefer the provider context or pass an explicit `QueryClient`.
|
||||
|
||||
## Reading Results
|
||||
|
||||
Lit Query controller creators return a callable accessor with a `current` property:
|
||||
|
||||
```ts
|
||||
const query = this.todos()
|
||||
const sameQuery = this.todos.current
|
||||
```
|
||||
|
||||
Render methods normally call the accessor:
|
||||
|
||||
```ts
|
||||
render() {
|
||||
const query = this.todos()
|
||||
|
||||
if (query.isPending) return html`Loading...`
|
||||
if (query.isError) return html`Error: ${query.error.message}`
|
||||
|
||||
return html`<todo-list .items=${query.data}></todo-list>`
|
||||
}
|
||||
```
|
||||
|
||||
## Reactive Options
|
||||
|
||||
If query options depend on host state, pass a function. Lit Query re-reads function accessors during host updates:
|
||||
|
||||
```ts
|
||||
class ProjectView extends LitElement {
|
||||
static properties = {
|
||||
projectId: { type: Number },
|
||||
}
|
||||
|
||||
projectId = 1
|
||||
|
||||
private readonly project = createQueryController(this, () => ({
|
||||
queryKey: ['project', this.projectId],
|
||||
queryFn: () => fetchProject(this.projectId),
|
||||
}))
|
||||
}
|
||||
```
|
||||
|
||||
If options are static, pass an object. If you mutate a static options object yourself, call the controller helper that causes the observer to see the new options, such as `refetch`, or prefer a function accessor for reactive state.
|
||||
|
||||
## Provider Context
|
||||
|
||||
Host-bound APIs can receive an explicit `QueryClient`, but most apps render under [`QueryClientProvider`](../reference/classes/QueryClientProvider.md). The provider uses Lit context to deliver the client to descendant controllers.
|
||||
|
||||
```ts
|
||||
customElements.define('query-client-provider', QueryClientProvider)
|
||||
```
|
||||
|
||||
```ts
|
||||
html`
|
||||
<query-client-provider .client=${queryClient}>
|
||||
<todos-view></todos-view>
|
||||
</query-client-provider>
|
||||
`
|
||||
```
|
||||
|
||||
Custom element registration is always the application's responsibility.
|
||||
|
||||
`QueryClientProvider` also registers its client in a process-local fallback store for [`useQueryClient`](../reference/functions/useQueryClient.md) and [`resolveQueryClient`](../reference/functions/resolveQueryClient.md). That fallback is intentionally conservative:
|
||||
|
||||
- If no provider is connected, `useQueryClient()` throws.
|
||||
- If exactly one distinct client is connected, `useQueryClient()` returns it.
|
||||
- If multiple distinct clients are connected in the same JavaScript context, `useQueryClient()` and `resolveQueryClient()` throw because the fallback would be ambiguous.
|
||||
|
||||
Multiple roots, micro-frontends, test suites with shared modules, and nested apps should avoid relying on the process-local fallback. Render host-bound controllers under the right provider, pass an explicit `QueryClient` to the controller, or cleanly disconnect providers between tests.
|
||||
Reference in New Issue
Block a user