### Install mvc-kit using npm
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
This command installs the mvc-kit library using npm. Ensure you have Node.js and npm installed on your system.
```bash
npm install mvc-kit
```
--------------------------------
### ViewModel Initialization vs. Component Loading (TSX)
Source: https://github.com/thermsio/mvc-kit/blob/master/BEST_PRACTICES.md
Demonstrates the recommended approach for initializing ViewModels. The 'Good' example shows the ViewModel handling its own initialization via `onInit()`, while the 'Bad' example illustrates a less efficient pattern where the component directly orchestrates loading using `useEffect`.
```tsx
// ✗ Bad: component orchestrates loading
function UsersPage() {
const [state, vm] = useLocal(UsersViewModel, { ... });
useEffect(() => { vm.load(); }, []);
return
...
;
}
// ✓ Good: ViewModel handles its own initialization via onInit()
function UsersPage() {
const [state, vm] = useLocal(UsersViewModel, { ... });
return
...
;
}
```
--------------------------------
### Async Initialization of a Service (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Illustrates how a Service can perform asynchronous operations during initialization by overriding the `onInit` hook. This example fetches configuration data before the service is ready to be used.
```typescript
class ConfigService extends Service {
private config: AppConfig | null = null;
protected async onInit() {
const res = await fetch('/api/config');
this.config = await res.json();
}
getConfig(): AppConfig {
return this.config!;
}
}
const service = new ConfigService();
await service.init();
```
--------------------------------
### Implement Controller Lifecycle Hooks - TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Controller.md
Provides examples of overriding the protected `onInit()` and `onDispose()` methods. `onInit()` is used for setup tasks like initiating subscriptions or starting timers upon controller initialization, while `onDispose()` is for specific teardown logic after the controller has been disposed.
```typescript
// Example for onInit (covered in other snippets)
// Example for onDispose:
protected onDispose() {
// Perform specific cleanup actions here
}
```
--------------------------------
### Implement Lifecycle Hooks (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
Demonstrates how to implement optional `onInit()` and `onDispose()` lifecycle hooks within a `ChatChannel` subclass. These methods allow for custom setup during initialization and final cleanup during disposal.
```typescript
class ChatChannel extends Channel {
protected onInit() {
// Set up external subscriptions, start timers, etc.
}
protected onDispose() {
// Final cleanup after everything else is torn down
}
}
```
--------------------------------
### Connect Channel and Handle Messages (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
An example within a ViewModel showing how to initialize a channel, connect to it, and set up a listener to handle incoming messages. This demonstrates a typical usage pattern for interacting with the channel in an application.
```typescript
// In a ViewModel using a channel
protected onInit() {
this.channel.on('message', (msg) => {
this.set({ messages: [...this.state.messages, msg] });
});
this.channel.connect();
}
```
--------------------------------
### React Hook: useInstance Signature and Usage Example
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-instance.md
Provides the TypeScript signature for the `useInstance` hook and a basic example of its usage in a child component to display state from a parent ViewModel.
```typescript
import { Subscribable } from 'your-library';
function useInstance(subscribable: Subscribable): Readonly;
// Example Usage:
// function ChildDisplay({ vm }: { vm: ParentViewModel }) {
// const state = useInstance(vm);
// return
{state.count}
;
// }
```
--------------------------------
### ViewModel State Management Example
Source: https://github.com/thermsio/mvc-kit/blob/master/CLAUDE.md
This example demonstrates the core concepts of the ViewModel class in mvc-kit, including state management, computed getters, and async tracking. It highlights how state is held internally and accessed via a getter, with derived values computed on the fly.
```typescript
// Example demonstrating ViewModel internals (conceptual)
class MyViewModel extends ViewModel<{
count: number;
}> {
constructor() {
super({ count: 0 });
}
// Computed getter
get doubledCount() {
return this.state.count * 2;
}
// Async method with tracking
async incrementAsync() {
await new Promise(resolve => setTimeout(resolve, 100));
this.set(state => ({ count: state.count + 1 }));
}
// Accessing async state
get incrementing() {
return this.async.incrementAsync;
}
}
// Usage within a React component (conceptual)
// const vm = useLocal(MyViewModel);
// console.log(vm.doubledCount);
// vm.incrementAsync();
// console.log(vm.incrementing.loading);
```
--------------------------------
### Service Class Example in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Demonstrates the Service class, a non-reactive infrastructure service typically used as a singleton. The example shows making an asynchronous API call with automatic cancellation if the service is disposed.
```typescript
class ApiService extends Service {
async fetchUser(id: string) {
// this.disposeSignal auto-cancels if the service is disposed
const res = await fetch(`/api/users/${id}`, { signal: this.disposeSignal });
return res.json();
}
}
```
--------------------------------
### Basic REST Service Implementation (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Provides an example of a `LocationService` implementing basic RESTful operations (getAll, create, delete). It utilizes `fetch` and `HttpError` for handling responses and errors, and accepts an `AbortSignal` for cancellation.
```typescript
class LocationService extends Service {
async getAll(signal?: AbortSignal): Promise {
const res = await fetch('/api/locations', { signal });
if (!res.ok) throw new HttpError(res.status, res.statusText);
return res.json();
}
async create(data: Omit, signal?: AbortSignal): Promise {
const res = await fetch('/api/locations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal,
});
if (!res.ok) throw new HttpError(res.status, res.statusText);
return res.json();
}
async delete(id: string, signal?: AbortSignal): Promise {
const res = await fetch(`/api/locations/${id}`, { method: 'DELETE', signal });
if (!res.ok) throw new HttpError(res.status, res.statusText);
}
}
```
--------------------------------
### Controller Class Example in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Illustrates the Controller class for orchestrating complex logic. It's stateless and component-scoped, with automatic disposal. The example shows setting up subscriptions and handling asynchronous operations with disposal cancellation.
```typescript
class CheckoutController extends Controller {
constructor(
private cart: CartViewModel,
private api: ApiService
) {
super();
}
// Called once after init() — set up subscriptions, wire dependencies
protected onInit() {
// subscribeTo registers auto-cleanup — no manual tracking needed
this.subscribeTo(this.cart, () => this.onCartChanged());
}
async submit() {
const items = this.cart.state.items;
// this.disposeSignal auto-cancels the request if the controller is disposed mid-flight
await this.api.checkout(items, { signal: this.disposeSignal });
this.cart.clear();
}
}
```
--------------------------------
### Pre-populating Singletons for Test Setup in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/src/singleton.md
Illustrates how to pre-populate shared singleton instances, like collections, before constructing a ViewModel in tests. This ensures that the ViewModel, when it resolves the same singleton, receives the pre-populated data, facilitating isolated and predictable test scenarios.
```typescript
beforeEach(() => teardownAll());
test('filtered getter applies search', () => {
const collection = singleton(UsersCollection);
collection.reset([
{ id: '1', firstName: 'Alice', status: 'on_duty' },
{ id: '2', firstName: 'Bob', status: 'off_duty' },
]);
const vm = new UsersViewModel({ items: [], search: '' });
vm.init();
// onInit subscribes to the same collection instance
expect(vm.state.items).toHaveLength(2);
vm.dispose();
});
```
--------------------------------
### Example: Auto-Initialization of Singleton Instance (React)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-singleton.md
Illustrates the auto-initialization behavior of `useSingleton`. When a singleton instance implements an `init` method, it is called automatically on mount. This initialization logic runs only once, even if multiple components mount using the same singleton.
```javascript
class InitVM extends ViewModel {
protected onInit() {
console.log('runs once, even with 5 components');
}
}
// All five components share one instance; onInit runs once
function App() {
return (
<>
>
);
}
```
```javascript
class ConfigService extends Service {
protected onInit() {
// Runs once on first mount
}
}
function Comp() {
const service = useSingleton(ConfigService);
// service.init() called automatically
}
```
--------------------------------
### ViewModel Class Example in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Illustrates advanced usage of the ViewModel class, including state updates with functions, lifecycle hooks (onInit, onSet, onDispose), and asynchronous operations with automatic disposal handling.
```typescript
class TodosViewModel extends ViewModel<{ items: string[] }> {
addItem(item: string) {
this.set(prev => ({ items: [...prev.items, item] }));
}
// Called once after init() — use for subscriptions, data fetching, etc.
protected onInit() {
this.loadItems();
}
// Called after every state change
protected onSet(prev: Readonly<{ items: string[] }>, next: Readonly<{ items: string[] }>) {
console.log('Items changed:', prev.items.length, '→', next.items.length);
}
protected onDispose() {
// Cleanup logic
}
// After init(), async methods are automatically tracked:
// vm.async.loadItems → { loading: boolean, error: string | null }
async loadItems() {
// this.disposeSignal is automatically aborted on dispose — fetch() will throw AbortError
const res = await fetch('/api/items', { signal: this.disposeSignal });
const items = await res.json();
this.set({ items });
}
}
```
--------------------------------
### Test ViewModel State and Filtered Results
Source: https://github.com/thermsio/mvc-kit/blob/master/BEST_PRACTICES.md
Demonstrates how to test a ViewModel by constructing it, initializing it, and asserting its state and derived properties like 'filtered'. This example shows setting search parameters and verifying the filtered results.
```typescript
import { singleton, teardownAll } from 'mvc-kit';
beforeEach(() => teardownAll());
test('filtered getter applies search', () => {
const collection = singleton(UsersCollection);
collection.reset([
{ id: '1', firstName: 'Alice', status: 'on_duty' },
{ id: '2', firstName: 'Bob', status: 'off_duty' },
]);
const vm = new UsersViewModel({ items: [], search: '', roleFilter: 'all' });
vm.init();
expect(vm.state.items).toHaveLength(2);
expect(vm.filtered).toHaveLength(2);
vm.setSearch('alice');
expect(vm.filtered).toHaveLength(1);
expect(vm.filtered[0].firstName).toBe('Alice');
vm.dispose();
});
```
--------------------------------
### Model Class Example in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Shows how to use the Model class for reactive entities with built-in validation and dirty tracking. It includes methods for setting state, defining validation rules, and managing the committed state.
```typescript
class UserModel extends Model<{ name: string; email: string }> {
setName(name: string) {
this.set({ name });
}
protected validate(state: { name: string; email: string }) {
const errors: Partial> = {};
if (!state.name) errors.name = 'Name is required';
if (!state.email.includes('@')) errors.email = 'Invalid email';
return errors;
}
}
const user = new UserModel({ name: '', email: '' });
console.log(user.valid); // false
console.log(user.errors); // { name: 'Name is required', email: 'Invalid email' }
user.setName('John');
console.log(user.dirty); // true (differs from committed state)
user.commit(); // Mark current state as baseline
user.rollback(); // Revert to committed state
```
--------------------------------
### Get Singleton Instance with useSingleton
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
The `useSingleton` hook retrieves a singleton instance managed by a registry. It automatically calls the `init()` method after the component mounts. This is useful for both subscribable singletons (like ViewModels) and service singletons.
```tsx
// Subscribable singleton
function UserProfile() {
const [state, vm] = useSingleton(UserViewModel);
return
{state.name}
;
}
// Service singleton
function Dashboard() {
const api = useSingleton(ApiService);
// ...
}
```
--------------------------------
### Quick Start: Counter ViewModel in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Demonstrates a basic CounterViewModel using mvc-kit. It extends the ViewModel class to manage a 'count' state and provides an 'increment' method to update it. Subscriptions allow reacting to state changes.
```typescript
import { ViewModel } from 'mvc-kit';
interface CounterState {
count: number;
}
class CounterViewModel extends ViewModel {
increment() {
this.set({ count: this.state.count + 1 });
}
}
const counter = new CounterViewModel({ count: 0 });
counter.subscribe((state, prev) => console.log(state.count));
counter.increment(); // logs: 1
```
--------------------------------
### ViewModel: Reactive State Container Example in TypeScript
Source: https://context7.com/thermsio/mvc-kit/llms.txt
Demonstrates the usage of the ViewModel class from mvc-kit for reactive state management. It covers state initialization, computed properties, lifecycle methods (onInit, onSet, onDispose), event emission, and automatic tracking of asynchronous operations. Dependencies include the 'mvc-kit' library.
```typescript
import { ViewModel } from 'mvc-kit';
interface TodoState {
items: string[];
filter: 'all' | 'active' | 'completed';
}
interface TodoEvents {
'item:added': { text: string };
'item:removed': { id: string };
}
class TodoViewModel extends ViewModel {
// Computed getter - automatically memoized after init()
get filteredItems(): string[] {
return this.state.filter === 'all'
? this.state.items
: this.state.items.filter(item => item.length > 0);
}
// Called once after init()
protected onInit() {
this.loadItems();
}
// Called after every state change
protected onSet(prev: Readonly, next: Readonly) {
console.log('State changed:', prev.items.length, '→', next.items.length);
}
// Cleanup logic
protected onDispose() {
console.log('ViewModel disposed');
}
addItem(text: string) {
this.set(prev => ({ items: [...prev.items, text] }));
this.emit('item:added', { text }); // Type-safe event emission
}
setFilter(filter: TodoState['filter']) {
this.set({ filter });
}
// Async methods are automatically tracked after init()
async loadItems() {
// disposeSignal auto-aborts on dispose - fetch throws AbortError
const res = await fetch('/api/items', { signal: this.disposeSignal });
const items = await res.json();
this.set({ items });
}
async saveItem(text: string) {
const res = await fetch('/api/items', {
method: 'POST',
body: JSON.stringify({ text }),
signal: this.disposeSignal,
});
return res.json();
}
}
// Usage
const vm = new TodoViewModel({ items: [], filter: 'all' });
vm.init(); // Must call init() to activate auto-tracking
// Subscribe to state changes
const unsubscribe = vm.subscribe((state, prev) => {
console.log('New state:', state);
});
// Subscribe to events
vm.events.on('item:added', ({ text }) => console.log('Added:', text));
// Use async tracking
vm.loadItems();
console.log(vm.async.loadItems); // { loading: true, error: null, errorCode: null }
await vm.loadItems();
console.log(vm.async.loadItems); // { loading: false, error: null, errorCode: null }
// Cleanup
vm.dispose();
```
--------------------------------
### Use Event and Emit Hooks for EventBus and ViewModel Communication in React
Source: https://context7.com/thermsio/mvc-kit/llms.txt
Demonstrates how to use `useEvent` to subscribe to events from an `EventBus` or `ViewModel` with automatic cleanup, and `useEmit` to get a stable reference to an emit function. This is useful for managing global state and inter-component communication.
```tsx
import { useEvent, useEmit, useLocal } from 'mvc-kit/react';
import { EventBus, ViewModel } from 'mvc-kit';
import { useState } from 'react';
interface AppEvents {
'toast': { message: string; type: 'success' | 'error' | 'info' };
'modal:open': { id: string };
'modal:close': void;
}
// Global event bus
const appEvents = new EventBus();
// Toast notifications using useEvent
function ToastContainer() {
const [toasts, setToasts] = useState>([]);
useEvent(appEvents, 'toast', ({ message, type }) => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id));
}, 3000);
});
return (
);
}
```
--------------------------------
### Initialize and Update Model State in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Model.md
This example shows how to initialize a Model with an initial state object and how to update the state using the protected `set()` method. The `set()` method accepts either a partial state object or a function for functional updates, ensuring immutability and notifying listeners of changes.
```typescript
const model = new UserFormModel({ name: 'Alice', email: 'alice@example.com', age: 30 });
// Reading state
console.log(model.state);
console.log(model.committed);
console.log(model.dirty);
// Updating state with a partial object
// Note: `set` is protected and should be called via subclass methods
// this.set({ name: 'Bob' });
// Updating state with a functional updater
// Note: `set` is protected and should be called via subclass methods
// this.set(prev => ({ age: prev.age + 1 }));
```
--------------------------------
### Singleton Service Registration and Teardown (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Demonstrates how to register and retrieve singleton instances of services using `mvc-kit`. It shows how to use `singleton`, `teardown`, and `teardownAll` for managing service lifecycles, particularly in ViewModel property initializers.
```typescript
import { singleton, teardown, teardownAll } from 'mvc-kit';
// Resolved inside ViewModels as property initializers
class LocationsViewModel extends ViewModel {
private service = singleton(LocationService);
}
// Same instance on repeated calls
singleton(LocationService) === singleton(LocationService); // true
// Teardown disposes and removes from registry
teardown(LocationService);
// Or tear down everything at once (common in test setup)
teardownAll();
```
--------------------------------
### Get Item by ID from Collection (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Collection.md
Shows the `get` query method, which performs an O(1) lookup for an item by its `id` using the collection's internal index. It returns the item if found, otherwise `undefined`.
```typescript
const user = collection.get(2);
```
--------------------------------
### Initialize Controller with Dependencies
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-local.md
Illustrates initializing a TimerController with a dependency ('roomId'). If 'roomId' changes, the controller instance is disposed and a new one is created.
```tsx
function Room({ roomId }: { roomId: string }) {
const ctrl = useLocal(() => new TimerController(roomId), [roomId]);
return
{ctrl.id}
;
}
```
--------------------------------
### useField Hook Usage Example (TypeScript/JSX)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-model.md
An example demonstrating how to use the `useField` hook within a React component to manage a single input field. It subscribes to the 'name' field of a form model, displaying the value, handling changes, and showing validation errors.
```typescript
import { useField } from 'mvc-kit/react';
function NameField({ model }: { model: FormModel }) {
const { value, error, set } = useField(model, 'name');
return (
set(e.target.value)} />
{error && {error}}
);
}
```
--------------------------------
### Initialize and Dispose Service Instance (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Shows basic usage of the `init` and `dispose` methods on a Service instance. It demonstrates that calling `init` multiple times is idempotent and that `dispose` also handles multiple calls gracefully.
```typescript
const service = new UserService();
service.init();
service.init(); // no-op
```
```typescript
service.dispose();
service.dispose(); // no-op
```
--------------------------------
### Initialize ViewModel with Multiple Dependencies
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-local.md
Shows initializing a ViewModel ('MultiVM') with multiple dependencies ('a', 'b'). If either 'a' or 'b' changes, the ViewModel instance is disposed and recreated.
```tsx
const [state, vm] = useLocal(MultiVM, { a, b, data: null }, [a, b]);
```
--------------------------------
### Conditional Dependencies in Getters (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/ViewModel.md
Demonstrates how getter dependencies update based on conditional branches. In this example, 'subtitle' is only a dependency when the mode is not 'compact'.
```typescript
get display(): string {
if (this.state.mode === 'compact') return this.state.title;
return `${this.state.title} — ${this.state.subtitle}`;
}
```
--------------------------------
### Initialize ViewModel with Factory and Dependencies
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-local.md
Demonstrates initializing a ChatViewModel using a factory function and providing a dependency ('roomId'). The ViewModel instance will be recreated if 'roomId' changes.
```tsx
const [state, vm] = useLocal(() => new ChatViewModel(roomId), [roomId]);
```
--------------------------------
### Defining ViewModel State Shape (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/ViewModel.md
Example of defining the state shape for a ViewModel using a TypeScript interface. The state must be a plain object.
```typescript
interface State {
items: Item[];
search: string;
typeFilter: 'all' | 'office' | 'warehouse';
}
class LocationsViewModel extends ViewModel {
// ...
}
```
--------------------------------
### Channel Initialization and Disposal (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
Shows the basic usage of initializing and disposing a `ChatChannel` instance. `init()` sets up the channel, and `dispose()` performs cleanup. Both methods are idempotent and handle cases where they are called multiple times or after disposal.
```typescript
const channel = new ChatChannel();
channel.init(); // sets initialized flag, calls onInit()
// ...use the channel...
channel.dispose(); // tears down everything
```
--------------------------------
### Happy Path Async Method Implementation
Source: https://github.com/thermsio/mvc-kit/blob/master/src/ViewModel.md
Shows the recommended way to implement async methods by focusing on the 'happy path' without manual try-catch blocks or loading flags. The framework handles async tracking, cancellation, and error reporting.
```typescript
async load() {
const data = await this.service.getAll(this.disposeSignal);
this.collection.reset(data);
}
```
--------------------------------
### Avoid Reactive State in Services (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Services are designed to be stateless infrastructure and should not manage reactive state or subscriptions. This example demonstrates an anti-pattern of a service attempting to be reactive.
```typescript
class BadReactiveService extends Service {
state = { loading: false }; // Services have no reactive state
listeners = new Set<() => void>(); // Services have no subscriptions
}
```
--------------------------------
### Singleton Channel Instance Management (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
Illustrates the singleton pattern for channel instances, ensuring only one instance of `ChatChannel` exists. It shows how to retrieve instances and demonstrates the effect of `teardownAll()` on disposing all singletons.
```typescript
const ch1 = singleton(ChatChannel);
const ch2 = singleton(ChatChannel);
ch1 === ch2; // true — same instance
teardownAll(); // disposes the channel along with all other singletons
```
--------------------------------
### Initialize ViewModel with Initial State and Dependencies
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-local.md
Shows how to initialize a UserViewModel with initial state and a dependency array. When the 'userId' dependency changes, the ViewModel instance is disposed and recreated.
```tsx
function UserPage({ userId }: { userId: string }) {
const [state, vm] = useLocal(UserViewModel, { userId, data: null }, [userId]);
const { loading, error } = vm.async.load;
return (
{loading && }
{error && }
{state.data && }
);
}
```
--------------------------------
### Avoid Caching Data in Services (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Services should be stateless and not cache data. Data caching should be handled by Collections. This example shows an anti-pattern where a service incorrectly caches user data.
```typescript
class BadService extends Service {
private cache: UserState[] = []; // belongs in UsersCollection
async getAll() {
if (this.cache.length) return this.cache;
this.cache = await fetch('/api/users').then(r => r.json());
return this.cache;
}
}
```
--------------------------------
### Collection Class Example in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/README.md
Demonstrates the Collection class for managing reactive arrays with CRUD operations, optimistic updates, and query methods. It allows efficient manipulation and querying of typed data collections.
```typescript
interface Todo {
id: string;
text: string;
done: boolean;
}
const todos = new Collection();
// CRUD (triggers re-renders)
todos.add({ id: '1', text: 'Learn mvc-kit', done: false });
todos.update('1', { done: true });
todos.remove('1');
todos.reset([...]); // Replace all
todos.clear();
// Optimistic update with rollback
const rollback = todos.optimistic(() => {
todos.update('1', { done: true });
});
// On failure: rollback() restores pre-update state
// Properties
todos.items; // readonly T[] (same as state)
todos.length; // number of items
// Query (pure, no notifications)
todos.get('1'); // Get by id (O(1) via internal index)
todos.has('1'); // Check existence
todos.find(t => t.done); // Find first match
todos.filter(t => !t.done);
todos.sorted((a, b) => a.text.localeCompare(b.text));
todos.map(t => t.text); // Map to new array
```
--------------------------------
### Service with Initialization Logic (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Demonstrates a `AuthService` that performs initialization logic in its `onInit` method, such as retrieving an authentication token from `localStorage`. It also includes a `setToken` method and cleans up the token in `onDispose`.
```typescript
class AuthService extends Service {
private token: string | null = null;
protected async onInit() {
this.token = localStorage.getItem('auth_token');
}
private get headers(): HeadersInit {
return this.token
? { Authorization: `Bearer ${this.token}`, 'Content-Type': 'application/json' }
: { 'Content-Type': 'application/json' };
}
async getCurrentUser(signal?: AbortSignal): Promise {
const res = await fetch('/api/me', { headers: this.headers, signal });
if (!res.ok) throw new HttpError(res.status, res.statusText);
return res.json();
}
setToken(token: string) {
this.token = token;
localStorage.setItem('auth_token', token);
}
protected onDispose() {
this.token = null;
}
}
```
--------------------------------
### Get or Create Singleton Instance (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/singleton.md
Retrieves an existing singleton instance for a given class or creates a new one if it doesn't exist or was disposed. Constructor arguments are only used during the initial creation.
```typescript
import { singleton } from "@mvc-kit/core";
// Get or create an instance of UserService
const service = singleton(UserService);
// Get or create an instance of UsersCollection
const collection = singleton(UsersCollection);
// First creation with arguments (e.g., initial state)
const vm1 = singleton(CounterViewModel, { count: 0 });
vm1.increment(); // count is now 1
// Subsequent calls ignore arguments, return the same instance
const vm2 = singleton(CounterViewModel, { count: 100 });
console.log(vm2 === vm1); // true
console.log(vm2.state.count === 1); // true
// If the instance was disposed, a new one is created
vm1.dispose();
const vm3 = singleton(CounterViewModel, { count: 0 });
console.log(vm3 === vm1); // false
console.log(vm3.disposed === false); // true
```
--------------------------------
### Provider and useResolve Hooks for Dependency Injection in React
Source: https://context7.com/thermsio/mvc-kit/llms.txt
Explains the `Provider` component for setting up dependency injection, particularly useful for testing and Storybook, and the `useResolve` hook to retrieve instances from the `Provider` context or the global singleton registry. `useSingleton` is also shown for managing ViewModel instances.
```tsx
import { Provider, useResolve, useSingleton } from 'mvc-kit/react';
import { ViewModel, Service } from 'mvc-kit';
interface UserState {
user: { id: string; name: string } | null;
}
class UserViewModel extends ViewModel {
setUser(user: UserState['user']) {
this.set({ user });
}
}
class ApiService extends Service {
async fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`, { signal: this.disposeSignal });
return res.json();
}
}
// Component using useResolve
function UserProfile() {
// Falls back to singleton() if not in Provider context
const api = useResolve(ApiService);
const [state] = useSingleton(UserViewModel, { user: null });
return
{state.user?.name ?? 'Loading...'}
;
}
// Production app - uses real singletons
function App() {
return (
);
}
// Test with mocks
function UserProfileTest() {
const mockApi = {
fetchUser: async () => ({ id: '1', name: 'Test User' }),
disposed: false,
disposeSignal: new AbortController().signal,
dispose: () => {},
} as unknown as ApiService;
const mockUserVM = new UserViewModel({ user: { id: '1', name: 'Test User' } });
return (
);
}
// Storybook story
export const WithMockedUser = () => {
const mockUserVM = new UserViewModel({
user: { id: '123', name: 'Storybook User' }
});
return (
);
};
```
--------------------------------
### Define a Service with API Fetching Methods (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Demonstrates how to extend the Service base class to create a custom service for fetching user data. It includes methods for retrieving all users and updating a specific user, handling HTTP errors and using AbortSignal for cancellation.
```typescript
import { Service, HttpError } from 'mvc-kit';
export class UserService extends Service {
async getAll(signal?: AbortSignal): Promise {
const res = await fetch('/api/users', { signal });
if (!res.ok) throw new HttpError(res.status, res.statusText);
return res.json();
}
async update(id: string, data: Partial, signal?: AbortSignal): Promise {
const res = await fetch(`/api/users/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
signal,
});
if (!res.ok) throw new HttpError(res.status, res.statusText);
return res.json();
}
}
```
--------------------------------
### Implementing onDispose for Service Teardown (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Provides an example of overriding the `onDispose` hook in a Service subclass to perform specific cleanup tasks. This method is called after the `disposeSignal` has been aborted and any registered cleanups have run.
```typescript
class StorageService extends Service {
private data = new Map();
protected onDispose() {
this.data.clear();
}
}
```
--------------------------------
### Register Cleanup Function (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
Provides an example of registering a cleanup function using `addCleanup()`. This function will be automatically executed when the channel is disposed, ensuring resources like external subscriptions are properly released.
```typescript
protected onInit() {
const unsub = externalService.on('event', this.handleEvent);
this.addCleanup(unsub);
}
```
--------------------------------
### Async Initialization with Cancellation in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Controller.md
Demonstrates a `BootstrapController` that performs asynchronous initialization by loading user authentication and configuration data concurrently using `Promise.all`. It passes the `disposeSignal` to async calls to ensure cancellation on teardown.
```typescript
class BootstrapController extends Controller {
private authService = singleton(AuthService);
private configService = singleton(ConfigService);
protected async onInit() {
const [user, config] = await Promise.all([
this.authService.getCurrentUser(this.disposeSignal),
this.configService.load(this.disposeSignal),
]);
// coordinate initial app state
}
}
```
--------------------------------
### Service Lifecycle Management
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
Illustrates the lifecycle of a Service, including initialization and disposal states. It highlights that calling `init()` after `dispose()` is a no-op, and `dispose()` before `init()` is valid.
```text
new Service() → disposed: false, initialized: false
│
init() → initialized: true, onInit() called
│
dispose() → disposed: true
1. abort signal
2. run cleanups
3. onDispose() called
```
--------------------------------
### Instance Creation and Usage with useLocal
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-instance.md
Demonstrates how useLocal manages the lifecycle of a ViewModel, creating, initializing, and disposing of it automatically. ChildDisplay uses useInstance to read from the managed ViewModel.
```tsx
function Parent() {
const [state, vm] = useLocal(ParentViewModel, { count: 0 });
return (
{/* vm is created, initialized, and disposed by useLocal */}
{/* ChildDisplay just reads from it */}
);
}
function ChildDisplay({ vm }: { vm: ParentViewModel }) {
const state = useInstance(vm);
return
{state.count}
;
}
```
--------------------------------
### Transport Abort Signal Event Listener (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Channel.md
Provides an example of attaching an event listener to an abort signal. This listener is crucial for cleaning up the WebSocket transport when the signal is aborted, typically during disconnect or dispose events.
```typescript
signal.addEventListener('abort', () => this.ws?.close());
```
--------------------------------
### Instantiate a Singleton Service in a ViewModel
Source: https://github.com/thermsio/mvc-kit/blob/master/BEST_PRACTICES.md
Shows how to instantiate a singleton Service within a ViewModel using the `singleton()` function. This ensures that only one instance of the service is created and reused across the application.
```typescript
import { singleton } from 'mvc-kit';
// ... inside a ViewModel class ...
private service = singleton(LocationService);
```
--------------------------------
### Run npm Tests and Builds
Source: https://github.com/thermsio/mvc-kit/blob/master/CLAUDE.md
These commands are used for running tests and building the project. 'npm test' runs all tests once, while 'npm run test:watch' runs tests in watch mode. 'npm run build' cleans the distribution directory, emits declarations, and bundles the project.
```bash
npm test
npm run test:watch
npm run build
```
--------------------------------
### TypeScript Controller Anti-Pattern: Resource Leaks
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Controller.md
Illustrates a common anti-pattern in TypeScript controllers: forgetting to clean up non-subscription resources, such as intervals. The example shows a leaked `setInterval` and suggests using `addCleanup` for proper resource management.
```typescript
// Don't forget to clean up non-subscription resources
class LeakyController extends Controller {
protected onInit() {
setInterval(() => this.tick(), 1000); // leaked — use addCleanup
}
}
```
--------------------------------
### Get Sorted Items from Collection (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Collection.md
Demonstrates the `sorted` query method, which returns a new array with the collection's items sorted according to the provided comparison function. The original collection is not modified.
```typescript
const byAge = collection.sorted((a, b) => a.age - b.age);
```
--------------------------------
### Recommended Singleton Initialization via Property Initializer in TypeScript
Source: https://github.com/thermsio/mvc-kit/blob/master/src/singleton.md
Highlights the best practice of resolving singletons using property initializers directly within the class definition. This makes the singleton instance available immediately upon ViewModel construction, contrasting with the less efficient approach of resolving them in `onInit`.
```typescript
// Good: available as soon as the ViewModel is constructed
private service = singleton(LocationService);
// Unnecessary: delays resolution to init time
protected onInit() {
this.service = singleton(LocationService);
}
```
--------------------------------
### Implement Optimistic Updates in a Collection
Source: https://github.com/thermsio/mvc-kit/blob/master/BEST_PRACTICES.md
Provides an example of implementing optimistic updates for a Collection in TypeScript. This pattern allows for instant UI feedback by updating the local cache before the asynchronous operation completes, with a rollback mechanism in case of failure.
```typescript
async toggleStatus(id: string) {
const rollback = this.collection.optimistic(() => {
this.collection.update(id, { status: 'done' });
});
try {
await this.service.update(id, { status: 'done' }, this.disposeSignal);
} catch (e) {
if (!isAbortError(e)) rollback();
throw e;
}
}
```
--------------------------------
### ViewModel Initialization with Collection Subscription (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Collection.md
Demonstrates how to initialize a ViewModel by subscribing to a collection and handling initial data loading. It ensures data is set immediately if available or fetched if the collection is empty. This pattern prevents redundant data fetches and ensures UI consistency.
```typescript
protected onInit() {
this.subscribeTo(this.collection, () => {
this.set({ items: this.collection.items });
});
if (this.collection.length > 0) {
this.set({ items: this.collection.items });
} else {
this.load();
}
}
```
--------------------------------
### Include AbortSignal in Async Service Methods (TypeScript)
Source: https://github.com/thermsio/mvc-kit/blob/master/src/Service.md
All asynchronous methods in services should accept an AbortSignal to allow for request cancellation. This prevents resource leaks when components unmount. The example shows a service method missing this crucial parameter.
```typescript
class BadService extends Service {
async getAll(): Promise {
const res = await fetch('/api/users'); // uncancellable — leaks on unmount
return res.json();
}
}
```
--------------------------------
### ViewModel Usage with Initial State in React
Source: https://github.com/thermsio/mvc-kit/blob/master/src/react/use-local.md
Demonstrates the common usage of the useLocal hook with a ViewModel class and its initial state. It shows how to access the state tuple and the ViewModel instance to manage component UI and actions.
```tsx
function LocationsPage() {
const [state, vm] = useLocal(LocationsViewModel, {
items: [],
search: '',
typeFilter: 'all',
});
const { loading, error } = vm.async.load;
return (