What You’ll Build
Create a counter model with observable properties that automatically updates a React component when changed. This tutorial demonstrates the core value of KOS: reactive, observable state that requires no manual UI synchronization.
Prerequisites
- Completed the workspace setup tutorial
- Development server running (use either method):
npm run dev(recommended – uses the scaffolded dev script)kosui dev(CLI shorthand)npx nx serve my-first-kos-app-ui(direct Nx command)
- Chrome DevTools Console tab open
Step 1: Generate Your Model
Generate a counter model:
kosui model --name counter --project my-first-kos-app-models --singleton[Screenshot: Terminal showing the kosui model command output with list of generated files]
The CLI creates:
counter-model.ts– Your model implementationindex.ts– Public exportstypes/index.d.ts– TypeScript types
[Screenshot: VS Code file explorer showing the counter folder with the three generated files]
Why singleton? We only need one counter instance for the entire app.
Step 2: Add Observable Properties
Open libs/my-first-kos-app-models/src/models/counter/counter-model.ts.
Find the class definition and add these properties:
@kosModel({ modelTypeId: MODEL_TYPE, singleton: true })
@kosLoggerAware()
export class CounterModelImpl implements IKosDataModel {
// Observable properties - components automatically re-render when these change
count: number = 0;
lastUpdated?: Date;
async init(): Promise<void> {}
async load(): Promise<void> {}
}[Screenshot: VS Code showing the CounterModelImpl class with count and lastUpdated properties added]
Why: These properties are automatically observable. When they change, any React component using them will re-render.
Step 3: Add Methods to Update Properties
Add methods that modify the observable properties:
@kosModel({ modelTypeId: MODEL_TYPE, singleton: true })
@kosLoggerAware()
export class CounterModelImpl implements IKosDataModel {
count: number = 0;
lastUpdated?: Date;
// Methods that update observable properties
increment(): void {
this.count++;
this.lastUpdated = new Date();
this.logger.info(`Counter incremented to ${this.count}`);
}
decrement(): void {
this.count--;
this.lastUpdated = new Date();
this.logger.info(`Counter decremented to ${this.count}`);
}
reset(): void {
this.count = 0;
this.lastUpdated = new Date();
this.logger.info('Counter reset to 0');
}
async init(): Promise<void> {}
async load(): Promise<void> {}
}[Screenshot: VS Code showing the three methods added to the class]
Why: These methods provide a clean API for modifying state. When components call these methods, the properties update and components automatically re-render.
Step 4: Add Computed Properties
Add computed properties using getters:
@kosModel({ modelTypeId: MODEL_TYPE, singleton: true })
@kosLoggerAware()
export class CounterModelImpl implements IKosDataModel {
count: number = 0;
lastUpdated?: Date;
// Computed property - automatically recalculates when count changes
get isPositive(): boolean {
return this.count > 0;
}
get isNegative(): boolean {
return this.count < 0;
}
get displayValue(): string {
if (this.isPositive) return `+${this.count}`;
return `${this.count}`;
}
increment(): void {
this.count++;
this.lastUpdated = new Date();
this.logger.info(`Counter incremented to ${this.count}`);
}
decrement(): void {
this.count--;
this.lastUpdated = new Date();
this.logger.info(`Counter decremented to ${this.count}`);
}
reset(): void {
this.count = 0;
this.lastUpdated = new Date();
this.logger.info('Counter reset to 0');
}
async init(): Promise<void> {
this.logger.info('Counter model initialized');
// Simulate external data changes (like device events)
setInterval(() => {
this.increment();
}, 2000); // Auto-increment every 2 seconds
}
async load(): Promise<void> {
this.logger.info('Counter model loaded');
}
}[Screenshot: VS Code showing the getter methods added above the increment/decrement/reset methods]
Why: Getters provide derived values that automatically update when their dependencies change. Components using displayValue will re-render whenever count changes.
Step 5: Register Your Model
Open apps/my-first-kos-app-ui/src/app/registration.ts.
Add the import at the top:
import { Counter } from "@my-first-kos-app/models";Add the registration after the existing registration lines:
KosModelRegistry.dispense.models().model(Counter).preload(Counter.type);[Screenshot: VS Code showing registration.ts with the Counter import and registration line highlighted]
Why: Registration tells KOS to create your model when the app starts. The preload ensures it’s ready before any components try to use it.
Step 6: Create a React Component
Create a new file: apps/my-first-kos-app-ui/src/app/components/counter-display.tsx
import { kosComponent } from '@kosdev-code/kos-ui-sdk';
import { useKosModel } from '@kosdev-code/kos-ui-sdk';
export const CounterDisplay = kosComponent(() => {
const { model: counter, ready } = useKosModel({
modelType: 'counter-model'
});
if (!ready) {
return <div>Loading counter...</div>;
}
return (
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h2>KOS Reactive Counter</h2>
<div style={{ fontSize: '48px', margin: '20px 0' }}>
{counter.displayValue}
</div>
<div style={{ marginBottom: '20px' }}>
<button onClick={() => counter.increment()} style={{ margin: '5px', padding: '10px 20px' }}>
Increment
</button>
<button onClick={() => counter.decrement()} style={{ margin: '5px', padding: '10px 20px' }}>
Decrement
</button>
<button onClick={() => counter.reset()} style={{ margin: '5px', padding: '10px 20px' }}>
Reset
</button>
</div>
<div style={{ fontSize: '14px', color: '#666' }}>
Status: {counter.isPositive ? 'Positive' : counter.isNegative ? 'Negative' : 'Zero'}
</div>
{counter.lastUpdated && (
<div style={{ fontSize: '12px', color: '#999', marginTop: '10px' }}>
Last updated: {counter.lastUpdated.toLocaleTimeString()}
</div>
)}
</div>
);
});[Screenshot: VS Code showing the complete CounterDisplay component]
Why kosComponent? This wrapper makes the component observe the model. When any referenced property changes (count, displayValue, isPositive, etc.), the component automatically re-renders.
Step 7: Add Component to Your App
Open apps/my-first-kos-app-ui/src/app/app.tsx and add the counter display:
import { CounterDisplay } from './components/counter-display';
export function App() {
return (
<div>
<h1>My First KOS App</h1>
<CounterDisplay />
</div>
);
}[Screenshot: VS Code showing the import and component usage in app.tsx]
Step 8: See It Work
Save all files. The browser should automatically refresh.
[Screenshot: Browser showing the counter UI with the count at 0, three buttons, and status text]
Watch the automatic updates:
As soon as the page loads, the counter will start incrementing automatically every 2 seconds. This simulates external data changes—like device events or API updates—happening without any user interaction.
What you’ll see:
- The count updates automatically every 2 seconds
- The display format changes (+1, +2, +3, etc.)
- The status changes to “Positive”
- The last updated time changes continuously
- No manual refresh needed—the UI just reacts
[Screenshot: Browser showing the counter at +5 with “Positive” status and a timestamp]
You can also click the Increment or Decrement buttons to change the count manually—both automatic and manual updates work seamlessly together.
Open the Console (F12) and you’ll see log messages streaming in:
Counter model initialized
Counter model loaded
Counter incremented to 1
Counter incremented to 2
Counter incremented to 3
Counter incremented to 4
...[Screenshot: Chrome DevTools Console showing the model lifecycle and continuous increment logs]
This demonstrates a key KOS concept: The model’s setInterval in the init() method simulates real-world scenarios where data comes from external sources (device sensors, network APIs, background processes). The UI automatically stays in sync—no manual updates, no event listeners in the component, no state management code.
What Just Happened
You built a complete reactive system:
- Observable Properties (
count,lastUpdated) – When these change, components re-render - Methods (
increment,decrement,reset) – Clean API for state updates - Computed Properties (
displayValue,isPositive) – Derived values that auto-update - Registration – Model created automatically when app starts
- React Integration –
useKosModelhook connects component to model - Automatic Reactivity – No useState, no useEffect, no manual synchronization
The component just reads model properties and calls model methods. KOS handles everything else.
Key Concepts Demonstrated
Observable State: Model properties are automatically tracked. Components re-render only when properties they use change.
No React State Management: Notice the component has no useState or useEffect. All state lives in the model.
Computed Values: Getters like displayValue automatically recalculate when their dependencies change.
Separation of Concerns: The model has all the logic. The component just renders and responds to user actions.
Type Safety: TypeScript ensures you can’t call methods that don’t exist or access wrong property types.
Try This
Make the counter more interesting:
- Add a
stepproperty to control increment/decrement amount - Add a
maxlimit that prevents incrementing beyond a value - Add a
doubleCountgetter that returnscount * 2 - Display all three values in the component
You’ll see that adding model features requires no changes to the component’s reactive logic—just add properties and getters, and they automatically work in the UI.
Next Steps
Now that you understand basic reactive models, explore:
- Device Integration – Connect models to real device APIs with
@kosTopicHandler - Configuration – Read device settings with
@kosConfigProperty - Containers – Manage multiple model instances with KosModelContainer
- Advanced Patterns – Model effects, dependencies, and relationships
Next: Learn how to connect models to device events and configuration.
