Web Loom logoWeb.loom
MVVM CoreMVVM in Solid

MVVM in Solid

How Web Loom ViewModels connect to SolidJS using createSignal and createEffect for RxJS subscriptions, or the from() helper for direct observable-to-signal conversion.

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: reactive primitives that push updates to dependents. The bridge is explicit and lightweight.


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 similar in spirit to @web-loom/signals-core, though different in API.


Option 1: createSignal + createEffect

The explicit subscription pattern mirrors the React useState + useEffect bridge:

// GreenhouseList.tsx
import { createSignal, createEffect, onCleanup, For, Show } from 'solid-js';
import { greenHouseViewModel, type GreenhouseData } from '@repo/view-models/GreenHouseViewModel';
 
export function GreenhouseList() {
  const [greenhouses, setGreenhouses] = createSignal<GreenhouseData[]>([]);
  const [isLoading, setIsLoading]     = createSignal(true);
 
  createEffect(() => {
    const subs = [
      greenHouseViewModel.data$.subscribe(data => setGreenhouses(data ?? [])),
      greenHouseViewModel.isLoading$.subscribe(setIsLoading),
    ];
 
    greenHouseViewModel.fetchCommand.execute();
 
    onCleanup(() => {
      subs.forEach(s => s.unsubscribe());
      greenHouseViewModel.dispose();
    });
  });
 
  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>
  );
}

onCleanup registers a function that Solid calls when the effect or component is disposed — the equivalent of React's useEffect cleanup return or Vue's onUnmounted.


Option 2: from() — Direct Observable to Signal Conversion

Solid provides a from() helper that converts any subscribable (including RxJS observables) into a Solid signal. This is the most concise integration:

import { from } from 'solid-js';
import { greenHouseViewModel } from '@repo/view-models/GreenHouseViewModel';
 
// Convert observables to Solid signals
const greenhouses = from(greenHouseViewModel.data$);
const isLoading   = from(greenHouseViewModel.isLoading$);

from() works because RxJS Observable implements the generic subscribable protocol: subscribe(next: (value: T) => void). Solid calls subscribe internally and manages the cleanup automatically within the component's reactive scope.

export function GreenhouseList() {
  const greenhouses = from(greenHouseViewModel.data$);
  const isLoading   = from(greenHouseViewModel.isLoading$);
 
  // Run fetch once when component mounts
  greenHouseViewModel.fetchCommand.execute();
 
  return (
    <Show when={!isLoading()} fallback={<p>Loading…</p>}>
      <For each={greenhouses() ?? []}>
        {(gh) => <li>{gh.name}</li>}
      </For>
    </Show>
  );
}

This is the preferred approach when you only need to read observable values without additional transformation.


Commands

Commands are plain async calls. Bind isExecuting$ using from():

import { from } from 'solid-js';
 
export function RefreshButton() {
  const isExecuting = from(greenHouseViewModel.fetchCommand.isExecuting$);
 
  return (
    <button
      onClick={() => greenHouseViewModel.fetchCommand.execute()}
      disabled={isExecuting()}
    >
      {isExecuting() ? 'Loading…' : 'Refresh'}
    </button>
  );
}

Signals Core Compatibility

@web-loom/signals-core and SolidJS signals share the same conceptual model: synchronous, fine-grained reactive values. If a ViewModel uses @web-loom/signals-core instead of RxJS BehaviorSubject, you can read its values directly in Solid's reactive contexts — though the write API differs and you'll still need an effect to bridge the two systems.

For ViewModels built on RxJS, stick with from() or the createSignal + createEffect pattern above.


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 statecreateSignal()
  • Subscribe to ViewModelcreateEffect() + onCleanup()
  • Convert observable to signalfrom(observable)
  • Cleanup on unmountonCleanup() inside the effect
  • Render list<For each={items()}>
  • Conditional render<Show when={condition}>
  • Commandsvm.someCommand.execute(payload)

The from() approach is concise and idiomatic for read-only observable binding. For more complex scenarios — derived state, combined observables, conditional subscriptions — use createEffect + onCleanup for full control.

Was this helpful?
Web Loom logoWeb.loom
Copyright © Web Loom. All rights reserved.