Components, controllers, and stores
Give renderable state, application behavior, and shared client state one clear owner each.
Begin with a plain component
A component that only transforms props into TSX should be a normal function. It is easier to read, test, and reuse because it owns no hidden behavior.
Create src/components/ProjectList.tsx
tsximport { Stack, Text } from "@tavojs/ui";
type ProjectSummary = { id: string; name: string };
export function ProjectList({ projects }: { projects: ProjectSummary[] }) {
return (
<Stack as="ul" gap="sm">
{projects.map((project) => (
<li>
<Text>{project.name}</Text>
</li>
))}
</Stack>
);
}
Add a local model for local interaction
When that mounted component needs renderable state, wrap it with createTavo and add a model. The filter below affects only this project list, so a global store would make ownership less clear.
Create src/components/ProjectFilter.tsx
tsximport { createTavo } from "@tavojs/core";
import { Button, Inline, Stack, Text } from "@tavojs/ui";
type Project = { id: string; name: string; active: boolean };
type ProjectFilterProps = { projects: Project[] };
type ProjectFilterState = { filter: "all" | "active" };
export const ProjectFilter = createTavo<ProjectFilterProps, ProjectFilterState>(
{
model: () => ({ filter: "all" }),
view: ({ props, state, model }) => {
const visibleProjects = props.projects.filter(
(project) => state.filter === "all" || project.active,
);
return (
<Stack gap="md">
<Inline gap="sm">
<Button onClick={() => model.patch({ filter: "all" })}>All</Button>
<Button onClick={() => model.patch({ filter: "active" })}>
Active
</Button>
</Inline>
{visibleProjects.map((project) => (
<Text>{project.name}</Text>
))}
</Stack>
);
},
},
);
Add a controller only for coordinated behavior
A Tavo.js component has a required view and optional model and controller. Add a controller when behavior needs lifecycle, cleanup, services, routing, subscriptions, or side effects. Keep small event handlers that only patch local model state in the view.
Make cleanup part of the behavior
Use onInit for subscriptions and initial model work, onMount or onLayout for DOM-dependent work, afterRender for post-commit behavior, and onPropsChange for explicit prop reactions. Return cleanup or register it with the controller so unmounting ends listeners and work.
Prefer controller helpers for events, store selection, resize, and intersection observation.
Subscribe to the smallest store value the component needs.
Keep request data in page or layout props instead of mirroring it into a process-wide store.