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:
- Bridge
Subscribable<T>to framework reactivity — convertget()/subscribe()into the framework's reactive primitive (React state, Vue ref, Solid signal, Svelte store, Angular signal/observable) - Auto-cleanup on unmount — unsubscribe when the component or scope is disposed
- Support SSR — provide scope context for per-request isolation via the framework's context/injection system
- 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.
| Framework | Scope Provider | Scope Consumer |
|---|---|---|
| React | <ScopeProvider scope={scope}> | useScopeContext() |
| Vue | app.use(stateloomPlugin, { scope }) or <ScopeProvider> | useScope() |
| Solid | <ScopeProvider scope={scope}> | useScope() |
| Svelte | setScope(scope) in parent component | getScope() |
| Angular | provideStateloomScope(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/injectviamakeEnvironmentProviders
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
| Framework | Primary Hook | Reactivity Bridge | Cleanup Mechanism | SSR Provider |
|---|---|---|---|---|
| React | useSyncExternalStore | External store protocol | Automatic (React lifecycle) | <ScopeProvider> (Context) |
| Vue | shallowRef + core effect() | Vue ref system | onScopeDispose | Plugin or <ScopeProvider> |
| Solid | createSignal + core effect() | Solid signal system | onCleanup | <ScopeProvider> (Context) |
| Svelte | Native store contract | subscribe protocol | Automatic ($store syntax) | setContext / getContext |
| Angular | Observable + Angular signal() | RxJS + Angular signals | DestroyRef.onDestroy | InjectionToken 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
| Concern | Strategy |
|---|---|
| React selector memoization | Cache (state, selection) pair; skip re-render when selection unchanged |
| Vue shallow refs | shallowRef avoids deep reactive wrapping of StateLoom-managed state |
| Solid equals: false | Disables Solid's built-in equality check (StateLoom handles equality upstream) |
| Svelte immediate call | Single get() call on subscribe — matches Svelte's expectation |
| Angular DestroyRef | Automatic cleanup; no manual unsubscribe needed |
| Adapter size | 50-200 lines each; tree-shakeable per hook |
| Zero duplication | Each adapter exports only framework-specific bindings, no shared runtime |
Cross-References
- React Adapter Design — React-specific bridging internals
- Vue Adapter Design — Vue-specific bridging internals
- Solid Adapter Design — Solid-specific bridging internals
- Svelte Adapter Design — Svelte-specific bridging internals
- Angular Adapter Design — Angular-specific bridging internals
- Architecture Overview — where adapters fit in the layer structure
- Core Design —
Subscribable<T>contract that all adapters bridge - Layer Scoping — adapter boundary rules (no business logic)