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:

query.module.ts
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:

query.ts
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:

chart.tsx
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.