View Model Introduction
View Model Introduction#
The term view model originates from the Model-View-ViewModel (MVVM) pattern. In practice, its greatest value lies in establishing a single "View Model" as the clear boundary between your application's state (business logic) and its view (React components).
Before we dive into the details of view model interfaces, let's take a step back and understand the general principles of view models.
What is a View Model?#
A view model is a type that describes the structure of the UI and the data that it displays. It is a type that is used to describe the state of the UI and the data that it displays. It is a type that is used to describe the actions that can be performed on the UI and the data that it displays.
General Principles#
1. Present Data as UI-Ready Strings
Every piece of data shown to the user should already be formatted when it reaches the UI.
- Don’t expose raw types like
Dateornumberdirectly; pre-format for display.- ❌ Bad:
datePosted: Date - ✅ Good:
postedAtDisplay: string - ❌ Bad:
amount: number, currency: string, currencySide: "left" | "right" - ✅ Good:
amountDisplay: string- For more complex UIs, you might use:
amountDisplay: { pre: string, number: { muted: boolean, text: string }[], post: string }
- For more complex UIs, you might use:
- ❌ Bad:
2. Never Expose Raw IDs to the UI
- Exposing IDs can lead the UI to leak business logic and is especially risky with AI-generated code.
- ✅ Good:
items: { key: string; onClick: () => void }[] - ❌ Bad:
onClickItem(id: string), items: { id: string }[]
- ✅ Good:
3. Let the View Model Structure Mirror the UI
- Shape your View Model and its documentation so their hierarchy and fields directly describe what appears in the UI.
- ❌ Bad:
historyManagement: { draftChanges, timelineScrubber } - ✅ Good:
topBar: { historyTimelineScrubber }, sidePanel: { draftChangeArea } - Tip: Use doc-comments to clarify any nuanced business behavior.
- ❌ Bad:
Practices That May Feel Wrong—But Are Worth It#
- Inline Reactivity Is Sometimes Best
Don’t arbitrarily split up components—often, it’s clearer to use an inline reactive renderer:
<Queryable queryable={vm.todos$}>
{todos => /* this is a valid hook context */}
</Queryable>- React Components on the View Model are OK (With a Caveat)
You can attach presentational React components (like icons or renderer functions) directly to your view model, so long as:
- The business logic remains fully testable.
- Those components are strictly for rendering/decoration.
icon: typeof Ta.Icon123;
renderDiff: (props) => React.ReactNode;- Prefer Booleans Over Discriminated Unions Where Possible
Replace unions-with-functions for simple UI controls (buttons, dropdowns, etc.) with simple booleans or shape-specific fields:
❌ Bad:
Atom<{ enabled: true, click: () => void } | { enabled: false, disableDisplayReason: string }>✅ Good:
disabledAtom: Atom<null | { displayReason: string }>click: () => void(always available, even if root-level event wiring is needed)
❌ Bad:
Atom<{ state: "open", keyDown: ("up" | "down") => void }>✅ Good:
options: []keyDown: (key: "up" | "down") => boolean
(returning boolean to signal event handling e.g. for stopping propagation)
- Keyboard/Event Handlers Should Return Booleans
Allow your keyboard or other event handlers to return a boolean indicating whether they've handled the event (to control propagation/default handling):
onKeyDown(event): boolean- Add an Opaque _dev Field for Debugging
Add a field like _dev: Record<string, unknown> to your model for UI-side debugging tools.
Just ensure its structure remains opaque.