MVVM in Solid
How Web Loom ViewModels connect to SolidJS using createSignal and onCleanup to bridge @web-loom/signals-core signals, matching the pattern used in the create-web-loom Solid template.
MVVM in Solid
Status: No demo app exists yet. This page documents the integration pattern for teams adopting Solid.
SolidJS uses fine-grained reactivity. Unlike React, Solid does not diff a virtual DOM or re-run component functions on state change. Instead, it creates reactive computation graphs at compile time — when a signal's value changes, only the specific DOM nodes reading that signal update. This makes Solid exceptionally fast.
Web Loom ViewModels integrate well with Solid because both systems share a similar philosophy: @web-loom/signals-core and SolidJS signals are both synchronous, pull-based, fine-grained reactive primitives. The bridge is explicit and lightweight — it's the same useSignalValue hook used in the create-web-loom Solid template.
Solid's Reactivity Primitives
import { createSignal, createEffect, createMemo, onCleanup } from 'solid-js';
const [count, setCount] = createSignal(0); // reactive value
const doubled = createMemo(() => count() * 2); // derived value
createEffect(() => {
console.log('count changed:', count()); // re-runs when count changes
});Signals are synchronous and pull-based — you call count() to read the value. This is nearly identical in spirit to @web-loom/signals-core's signal()/.get().
The useSignalValue Bridge
The create-web-loom Solid template ships a small hook that mirrors a Web Loom signal into a Solid signal, syncing on every change and cleaning up via onCleanup:
// src/hooks/useObservable.ts
import { createSignal, onCleanup } from 'solid-js';
type SignalLike<T> = {
get: () => T;
subscribe: (fn: () => void) => () => void;
};
export function useSignalValue<T>(signal: SignalLike<T>) {
const [value, setValue] = createSignal(signal.get());
const sync = () => {
setValue(signal.get());
};
sync(); // seed with the current value immediately
const unsubscribe = signal.subscribe(sync);
onCleanup(unsubscribe);
return value;
}sync() is called once immediately (since sig.subscribe() only fires on future changes), then registered as the change handler. onCleanup — Solid's equivalent of React's useEffect cleanup or Vue's onUnmounted — unsubscribes when the owning component or effect is disposed.
// GreenhouseList.tsx
import { For, Show } from 'solid-js';
import { greenHouseViewModel, type GreenhouseData } from '@repo/view-models/GreenHouseViewModel';
import { useSignalValue } from '../hooks/useObservable';
export function GreenhouseList() {
const greenhouses = useSignalValue(greenHouseViewModel.data$);
const isLoading = useSignalValue(greenHouseViewModel.isLoading$);
greenHouseViewModel.fetchCommand.execute(); // fires once when the component initializes
return (
<div>
<Show when={!isLoading()} fallback={<p>Loading…</p>}>
<ul>
<For each={greenhouses() ?? []}>
{(gh) => (
<li>
{gh.name}
<button onClick={() => greenHouseViewModel.deleteCommand.execute(gh.id!)}>
Delete
</button>
</li>
)}
</For>
</ul>
</Show>
</div>
);
}Commands
Commands are plain async calls. Bind isExecuting$ with the same useSignalValue hook:
import { useSignalValue } from '../hooks/useObservable';
export function RefreshButton() {
const isExecuting = useSignalValue(greenHouseViewModel.fetchCommand.isExecuting$);
return (
<button
onClick={() => greenHouseViewModel.fetchCommand.execute()}
disabled={isExecuting()}
>
{isExecuting() ? 'Loading…' : 'Refresh'}
</button>
);
}A Note on Solid's from()
Solid ships a built-in from() helper that converts any object with a subscribe(fn) method into a Solid signal — tempting to reach for here since Web Loom signals already have that shape. It's not a drop-in fit, though: from() seeds its Solid signal with undefined and waits for the first callback invocation to get a value, but sig.subscribe() only fires on future changes, not the current one. Used directly, from(greenHouseViewModel.data$) would stay undefined until the next change — useSignalValue above exists specifically to close that gap by calling sync() once up front.
SolidStart Considerations
SolidStart is Solid's meta-framework for SSR and full-stack apps. ViewModel HTTP calls should be client-only — wrap the subscription setup in onMount or inside a client-side createEffect guarded by isServer:
import { isServer } from 'solid-js/web';
if (!isServer) {
greenHouseViewModel.fetchCommand.execute();
}For server-loaded initial data, use SolidStart's createAsync / query patterns for the initial load and hand the result to the ViewModel's initial state.
Summary
- Reactive state —
createSignal() - Bridge a ViewModel signal —
useSignalValue(vm.data$)— seeds from.get(), then.subscribe() - Cleanup on unmount —
onCleanup()inside the hook - Render list —
<For each={items()}> - Conditional render —
<Show when={condition}> - Commands —
vm.someCommand.execute(payload)
useSignalValue is the one hook every component needs — it's small enough to keep inline and covers data$, isLoading$, error$, and Command signals identically.