Olas keeps the fetching, the state and the rules of each screen in controllers: plain TypeScript functions. React, Vue or Svelte draws what a controller exposes, and a test runs it with no renderer.
Components
draw what they read
Controllers
own the state and logic
A counter is the smallest thing Olas can build. A full screen, with queries, forms and child controllers, follows the same three steps.
A controller is a function that returns an object. That object is everything the screen can read and do. Nothing in it imports React, Vue or Svelte.
import { computed, defineController, signal } from '@kontsedal/olas-core'
export const counter = defineController(() => {
const count = signal(0)
const double = computed(() => count.value * 2)
return {
count,
double,
increment: () => count.update((n) => n + 1),
}
})createRoot(counter) builds the controller once, outside the view. A component reads its signals and calls its methods. The component keeps no state and makes no decision of its own.
import { useRoot, useValue } from '@kontsedal/olas-react'
export function Counter() {
const { count, increment } = useRoot()
return <button onClick={increment}>Clicked {useValue(count)} times</button>
}The test calls the controller the way a component would. It runs in Node, with no DOM, no jsdom and no act.
import { createTestController } from '@kontsedal/olas-core/testing'
import { expect, test } from 'vitest'
import { counter } from './counter'
test('counts clicks', () => {
const { api } = createTestController(counter, { deps: {} })
api.increment()
expect(api.count.value).toBe(1)
expect(api.double.value).toBe(2)
})@kontsedal/olas-core is one package with no framework inside it. These are the parts a controller builds with.
Dispose a controller and everything it created goes with it: children, effects, queries and forms. There is no cleanup to forget.
Two controllers that ask for the same key share one cache entry and one fetch. Stale times, retries, cancellation and pagination are built in.
Write to the cache before the server answers. A failed write puts the cache back in order, and a concurrency mode decides what a second click does.
Fields, forms and field arrays, with sync and async validators, server errors and a submit that returns a typed result.
One root per request, so one user's data cannot reach another's page. Hydrate on the client, or stream each query into the page as it settles.
Every package is optional apart from the core and one adapter. Each one installs on its own.
Next, Getting started builds a todo list with a query, a view and a test.