@penio/lit-rtk-query
v0.3.1
Published
Lit reactive controllers and hooks for RTK Query and Redux state integration
Maintainers
Readme
lit-rtk-query
Lit reactive controllers and hooks that integrate RTK Query and Redux state into Lit web components.
- Zero boilerplate — drop a controller or generated hook into your element and the query lifecycle (loading, caching, re-fetching, cleanup) is handled automatically.
- Precise re-renders — updates are gated on reference equality so unrelated Redux dispatches never trigger spurious renders.
- Dual-Input Parameter Tracking — derive query arguments seamlessly from both global Redux state and local Lit component properties simultaneously.
- Full TypeScript inference — UI state is accurately narrowed to your endpoint's exact shapes without intrusive inline type assertions.
Installation
npm install @penio/lit-rtk-queryPeer dependencies (install alongside if not already present):
npm install lit @reduxjs/toolkitUsage
RTKQueryController — class-based
The controller orchestrates cache subscriptions and binds updates directly to the component. State properties are exposed under the .state object.
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { RTKQueryController } from '@penio/lit-rtk-query';
import { store } from './store.js';
import { api } from './api.js';
@customElement('product-list')
class ProductList extends LitElement {
@state() private _page = 1;
products = new RTKQueryController(
this,
() => store,
api.endpoints.getProducts,
{
// Receives both global Redux state AND the Lit host element instance
argSelector: (state, host) => ({
storeId: state.auth.user?.merchant ?? 'DEFAULT',
page: host._page
})
},
);
render() {
const { data = [], isLoading, error } = this.products.state ?? {};
if (isLoading) return html`<p>Loading…</p>`;
if (error) return html`<p>Error</p>`;
return html`<ul>${data.map(p => html`<li>${p.name}</li>`)}</ul>`;
}
}createLitHooks — generate hooks from an entire RTK Query API
Generate specialized, type-safe functional hooks for your components. To avoid initialization race conditions with this in TypeScript class fields, initialize query hooks cleanly inside your component constructor.
import { LitElement, html } from 'lit';
import { state } from 'lit/decorators.js';
import { createLitHooks, type UseQueryResult, type UseMutationResult } from '@penio/lit-rtk-query';
import { api } from './api.js';
// Generates useGetProducts, useCreateProduct, etc. from api.endpoints
const { useGetProducts, useCreateProduct } = createLitHooks(api);
class ProductList extends LitElement {
@state() private _page = 1;
// Declare properties at the class level
products: UseQueryResult<any>;
create: UseMutationResult<any>;
constructor() {
super();
// Initialize hooks safely in the constructor where 'this' context is fully baked
this.products = useGetProducts(this, (state, host) => ({
storeId: state.auth.user?.merchant ?? 'DEFAULT',
page: host._page,
}));
this.create = useCreateProduct(this);
}
render() {
const { data = [], isLoading } = this.products;
return html`
<button @click=${() => this.create.trigger({ name: 'Widget' })}>
Add Widget
</button>
${isLoading ? html`<p>Loading...</p>` : ''}
<ul>${data.map(p => html`<li>${p.name}</li>`)}</ul>
`;
}
}Note:
useGetProducts(query hooks) expose state directly on the result (.data,.isLoading,.error,.refetch()), while theRTKQueryControllerclass exposes the same data nested under.state. Mutation hooks likeuseCreateProductalways require an explicit.trigger(args)call — they never auto-fire on mount.
SelectorController — generic Redux selector
Subscribes to any slice of Redux state without coupling to RTK Query:
import { SelectorController } from '@penio/lit-rtk-query';
import { store } from './store.js';
class CartBadge extends LitElement {
count = new SelectorController(
this,
() => store,
(state: AppState) => state.cart.itemCount,
);
render() {
return html`<span>${this.count.value}</span>`;
}
}Pass a custom equality function as the fourth argument to avoid re-renders when an object reference changes but the values remain identical:
import { shallowEqual } from '@reduxjs/toolkit';
summary = new SelectorController(this, () => store, selectSummary, shallowEqual);API Reference
RTKQueryController<TArg, TData, THost>
| Member | Type | Description |
| :--- | :--- | :--- |
| state | QueryState<TData> \| undefined | Combined UI lifecycle states (data, isLoading, isFetching, error, etc.) |
| refetch() | void | Forces the query to update, bypassing the Redux cache |
| hostConnected() | void | Automatically handles Redux store subscription mapping on element mount |
| hostDisconnected() | void | Unsubscribes and frees up memory automatically on unmount |
UseQueryResult<TData> (returned by useRTKQuery / generated use* query hooks)
| Member | Type | Description |
| :--- | :--- | :--- |
| value | QueryState<TData> \| undefined | The full underlying query state object |
| data | TData \| undefined | Shortcut to the resolved data |
| isLoading | boolean | True while the initial fetch is in flight |
| isFetching | boolean | True while any fetch (initial or refetch) is in flight |
| error | unknown | Populated if the query rejected |
| refetch() | void | Forces the query to update, bypassing the Redux cache |
UseMutationResult<TArg, TData> (returned by generated mutation hooks)
| Member | Type | Description |
| :--- | :--- | :--- |
| isLoading | boolean | True while the mutation request is in flight |
| data | TData \| null | Result of the last successful mutation |
| error | unknown | Populated if the last mutation rejected |
| trigger(args) | Promise<void> | Dispatches the mutation; never fires automatically |
SelectorController<TState, TSelected, THost>
| Member | Type | Description |
| :--- | :--- | :--- |
| value | TSelected \| undefined | Current selected value; undefined until connected |
RTKQueryOptions<THost, TArg>
| Option | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| argSelector | (state: any, host: THost) => TArg | () => undefined | Evaluates parameters dynamically from both Redux and local Lit properties |
| lazy | boolean | false | Skips the automatic evaluation/fetching sequence on connect. Query hooks only — mutation hooks always require an explicit trigger() call regardless of this option. |
Development
npm install
npm run build # Compile TypeScript to dist/
npm run build:watch # Compile in watch mode
npm run typecheck # Type-check without emitting
npm test # Run the browser test suite (@web/test-runner)Contributing
Issues and pull requests are welcome. Please make sure npm run typecheck and npm test pass before opening a PR.
License
Licensed under the Apache License, Version 2.0.
