Skip to content

createViewModel

createViewModel combines multiple Observable sources into a single projected view model using combineLatest, map, and distinctUntilChanged. It is the recommended way to compose data from several stores or streams into a UI-ready shape that only emits when the projected output actually changes.

import { createViewModel } from "rxjs-ai";
import { createStore, createViewModel } from "rxjs-ai";
// Two independent stores
const userStore = createStore({ name: "Alice", role: "admin" });
const uiStore = createStore({ theme: "dark", sidebarOpen: true });
// Combine selectors into a single view model
const vm$ = createViewModel(
{
userName: userStore.select((s) => s.name),
role: userStore.select((s) => s.role),
theme: uiStore.select((s) => s.theme),
sidebarOpen: uiStore.select((s) => s.sidebarOpen),
},
(sources) => ({
greeting: `Hello, ${sources.userName}`,
isAdmin: sources.role === "admin",
theme: sources.theme,
sidebarOpen: sources.sidebarOpen,
}),
);
vm$.subscribe((vm) => {
console.log(vm.greeting); // "Hello, Alice"
console.log(vm.isAdmin); // true
console.log(vm.theme); // "dark"
});

The returned Observable only emits when the projected output changes, preventing unnecessary side-effects downstream.

By default, createViewModel uses Object.is on the projected result to determine whether a new emission should be sent to subscribers. For projected objects (which create a new reference on every emission), you should provide a custom comparator:

import { isEqual } from "lodash-es";
const vm$ = createViewModel(
{
items: store.select((s) => s.items),
filter: store.select((s) => s.filter),
},
({ items, filter }) => ({
visibleItems: items.filter((item) => item.category === filter),
totalCount: items.length,
}),
isEqual, // deep equality prevents duplicate emissions
);

You can also use a field-level comparator for more granular control:

const vm$ = createViewModel(
{
items: store.select((s) => s.items),
query: store.select((s) => s.query),
},
({ items, query }) => ({
results: items.filter((i) => i.name.includes(query)),
query,
}),
(prev, next) =>
prev.query === next.query && prev.results.length === next.results.length,
);
function createViewModel<
Sources extends ObservableMap,
ViewModel,
>(
sources: Sources,
projector: (values: ObservableValues<Sources>) => ViewModel,
compare: (previous: ViewModel, current: ViewModel) => boolean = Object.is,
): Observable<ViewModel>;
// where
type ObservableMap = Record<string, Observable<unknown>>;
ParameterTypeDescription
sourcesObservableMap (Record<string, Observable<unknown>>)A named map of Observable sources. Each key becomes a property on the object passed to the projector.
projector(values: ObservableValues<Sources>) => ViewModelA pure function that receives the latest values from all sources and returns the projected view model shape.
compare(previous: ViewModel, current: ViewModel) => booleanEquality function used by distinctUntilChanged. Defaults to Object.is. Return true when the two values should be considered equal (suppressing the emission).

Returns: Observable<ViewModel> — an Observable that emits the projected view model whenever any source emits and the projected output differs from the previous emission.