@penio/lit-rtk-query
v0.2.0
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 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 '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 } from '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: 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.state ?? {};
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>
`;
}
}SelectorController — generic Redux selector
Subscribes to any slice of Redux state without coupling to RTK Query:
import { SelectorController } from '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 |
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 |
License
MIT
