Skip to content

Framework Adapters Overview

Cross-cutting patterns shared by all framework adapters — the adapter contract, bridging strategies, SSR scope integration, and selection guidance. Per-framework implementation details live in individual adapter design docs.

Overview

Framework adapters are thin bridges (50-200 lines each) that connect the universal Subscribable<T> contract from @stateloom/core to framework-specific reactivity. Each adapter peer-depends on @stateloom/core and its target framework. Adapters contain no business logic — they are pure bridging code.

Every signal, computed, store, and atom implements Subscribable<T>. Framework adapters bridge this single interface to framework-specific reactivity, which means any adapter works with any paradigm without coupling.

Adapter Contract

Every adapter must satisfy four requirements:

  1. Bridge Subscribable<T> to framework reactivity — convert get()/subscribe() into the framework's reactive primitive (React state, Vue ref, Solid signal, Svelte store, Angular signal/observable)
  2. Auto-cleanup on unmount — unsubscribe when the component or scope is disposed
  3. Support SSR — provide scope context for per-request isolation via the framework's context/injection system
  4. No business logic — purely mechanical bridging, no state management features

Adapters are intentionally minimal. They delegate all reactive behavior to @stateloom/core and all paradigm-specific behavior to the paradigm packages. The adapter's only job is lifecycle management and type conversion.

Framework Bridge Pattern

All adapters follow the same structural pattern despite different framework APIs. This class diagram shows the shared structure:

Each adapter exports:

  • Signal hook/function — bridges a raw Subscribable<T> to the framework's reactive primitive
  • Store hook/function — bridges a StoreApi<T> with optional selector and equality check
  • Scope mechanism — provides SSR scope via the framework's context/injection system

The naming follows framework conventions: React and Vue use useX composable/hook naming, Solid uses useX accessors, Svelte uses toReadable/toWritable to match its store contract, and Angular uses toObservable/toAngularSignal/injectStore to match its RxJS and signal ecosystem.

Reactivity Bridge Strategies

Each framework has a different reactivity system that requires a distinct bridging strategy:

React: External Store Protocol

React 18's concurrent rendering can "tear" — reading different values during a single render pass. useSyncExternalStore is the only safe way to subscribe to external stores in concurrent mode. The adapter maps Subscribable.subscribe() to the subscribe parameter and Subscribable.get() to the getSnapshot parameter.

Vue and Solid: Core effect()

Using subscribe() alone would miss computed-to-computed dependency chains. Core effect() participates in the full reactive graph, ensuring all dependencies are properly tracked. The effect's cleanup is handled by framework-specific disposal hooks (onScopeDispose for Vue, onCleanup for Solid).

Vue uses shallowRef (not ref) to avoid deep reactive wrapping of StateLoom-managed state. Solid uses createSignal with { equals: false } because StateLoom handles equality upstream.

Svelte: Native Store Contract

Svelte's $store syntax works with any object that has a subscribe method matching the Svelte store contract. By returning objects that satisfy Readable<T> and Writable<T>, StateLoom signals work directly with Svelte's template syntax — no special compiler support needed.

The key difference from Subscribable<T> is that Svelte's contract requires subscribe to call the callback immediately with the current value, while StateLoom's subscribe() only fires on changes. The toReadable function bridges this gap by calling get() first.

Angular: Multiple Bridges

Angular's ecosystem uses both RxJS Observables (for async pipes and operators) and Angular Signals (for template bindings). toObservable serves the RxJS path, toAngularSignal serves the Signals path, and injectStore provides a higher-level API for the common case of store-to-template binding. fromObservable bridges in the reverse direction — RxJS into StateLoom — with lazy subscription and reference counting.

SSR Integration Patterns

All adapters follow the same SSR pattern: provide a scope via the framework's context/injection system, and read it in components that need scoped state.

FrameworkScope ProviderScope Consumer
React<ScopeProvider scope={scope}>useScopeContext()
Vueapp.use(stateloomPlugin, { scope }) or <ScopeProvider>useScope()
Solid<ScopeProvider scope={scope}>useScope()
SveltesetScope(scope) in parent componentgetScope()
AngularprovideStateloomScope(scope)injectScope()

Each adapter uses the framework's idiomatic context/injection mechanism:

  • React: createContext / useContext
  • Vue: InjectionKey / provide / inject (via plugin, composable, or component)
  • Solid: createContext / useContext (implemented without JSX to avoid build-time transform requirements)
  • Svelte: setContext / getContext (with try-catch for Svelte 4 compatibility)
  • Angular: InjectionToken / inject via makeEnvironmentProviders

SSR Hydration Flow

This sequence shows the complete server-to-client hydration flow across all adapters:

The flow is identical regardless of framework — the adapter merely translates the scope injection into the framework's context system. The serialization format is framework-agnostic, enabling shared SSR infrastructure across different framework adapters.

Adapter Selection Guide

FrameworkPrimary HookReactivity BridgeCleanup MechanismSSR Provider
ReactuseSyncExternalStoreExternal store protocolAutomatic (React lifecycle)<ScopeProvider> (Context)
VueshallowRef + core effect()Vue ref systemonScopeDisposePlugin or <ScopeProvider>
SolidcreateSignal + core effect()Solid signal systemonCleanup<ScopeProvider> (Context)
SvelteNative store contractsubscribe protocolAutomatic ($store syntax)setContext / getContext
AngularObservable + Angular signal()RxJS + Angular signalsDestroyRef.onDestroyInjectionToken provider

Design Decisions

Why useSyncExternalStore for React

React 18's concurrent rendering can "tear" — reading different values during a single render pass. useSyncExternalStore is the only safe way to subscribe to external stores in concurrent mode. It handles both the synchronous read and the subscription lifecycle.

Why Core effect() for Vue and Solid

Using subscribe() alone would miss computed-to-computed dependency chains. Core effect() participates in the full reactive graph, ensuring all dependencies are properly tracked. The effect's cleanup is handled by framework-specific disposal hooks (onScopeDispose, onCleanup).

Why Svelte Uses the Native Store Contract

Svelte's $store syntax works with any object that has a subscribe method matching the Svelte store contract. By returning objects that satisfy Readable<T> and Writable<T>, StateLoom signals work directly with Svelte's template syntax — no special compiler support needed.

Why Angular Has Multiple Bridging Functions

Angular's ecosystem uses both RxJS Observables (for async pipes and operators) and Angular Signals (for template bindings). toObservable serves the RxJS path, toAngularSignal serves the Signals path, and injectStore provides a higher-level API for the common case of store-to-template binding.

Performance Considerations

ConcernStrategy
React selector memoizationCache (state, selection) pair; skip re-render when selection unchanged
Vue shallow refsshallowRef avoids deep reactive wrapping of StateLoom-managed state
Solid equals: falseDisables Solid's built-in equality check (StateLoom handles equality upstream)
Svelte immediate callSingle get() call on subscribe — matches Svelte's expectation
Angular DestroyRefAutomatic cleanup; no manual unsubscribe needed
Adapter size50-200 lines each; tree-shakeable per hook
Zero duplicationEach adapter exports only framework-specific bindings, no shared runtime

Cross-References