Integrating libraries
The framework's answer to "how do I use library X" is almost always the same two moves: provide the client object through DI, and bridge its subscribe API into a signal. No adapter packages needed.
Move 1: the client is a provider
Any library with a client/store/registry object becomes a module provider — usually via a small forRoot module so config travels with it:
import { QueryClient, type QueryClientConfig } from '@tanstack/preact-query';
// Same pattern as RouterModule.forRoot: an object carrying __module
// metadata can sit in 'imports' — no decorator needed.
export class QueryModule {
static forRoot(config: QueryClientConfig = {}) {
const client = new QueryClient(config);
return {
__module: {
providers: [{ provide: QueryClient, useValue: client }],
},
};
}
}
// app.module.ts
@Module({ imports: [QueryModule.forRoot()] })
export class AppModule {}Views resolve it with inject(QueryClient) — the class itself is the token (an InjectionToken is only needed for non-class values or aliases). Testing swaps one provider.
Move 2: subscribe → signal
Most libraries expose subscribe(fn) → unsubscribe somewhere. Bridge that into a signal and every view reading it gets fine-grained reactivity; dispose through the calling scope's DestroyRef and cleanup is automatic:
import { signal, getCurrentInjector, DestroyRef } from '@preact-mvc/core';
import { QueryObserver, type QueryObserverOptions } from '@tanstack/preact-query';
// Call inside construction — the scope's DestroyRef owns the observer.
export function querySignal<T>(
client: QueryClient,
opts: QueryObserverOptions<T> | (() => QueryObserverOptions<T>),
) {
const getOpts = typeof opts === 'function' ? opts : () => opts;
const observer = new QueryObserver(client, getOpts());
const s = signal(observer.getCurrentResult());
const unsub = observer.subscribe((r) => (s.value = r));
getCurrentInjector()?.get(DestroyRef).onDestroy(unsub);
// Options getter → tracked effect re-keys the same observer on signal
// writes (reactive query keys, zero leaked subscriptions).
if (typeof opts === 'function') effect(() => observer.setOptions(getOpts()));
return s;
}This is the general recipe: construct the library object, subscribe into a signal, hand teardown to DestroyRef. The same shape wraps WebSockets, RxJS
Observables, Firebase listeners, media queries — anything with a callback.
Move 3 (rare): lifecycle-bound wrappers
Libraries that need a DOM element — ECharts, maps, editors — can't be injectables (no element at construction). The pattern is a tiny function component that owns both the element ref and the instance:
import { useRef, useEffect } from 'preact/hooks';
import * as echarts from 'echarts';
export function Chart({ option }: { option: echarts.EChartsOption }) {
const el = useRef<HTMLDivElement>(null);
useEffect(() => {
const chart = echarts.init(el.current!);
chart.setOption(option);
return () => chart.dispose(); // element-bound cleanup
}, [option]);
return <div ref={el} style="width:100%;height:100%" />;
}Rules of thumb: stateful client? provider. Push API? signal bridge. DOM-bound? component wrapper. For worked examples, see data fetching and the enterprise demo.