Skip to content

App logic that lives outside your components

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

  • App
  • Toolbar
  • BoardPage

Controllers

own the state and logic

  • approot
  • toolbar
  • board
  • createQuery
  • createMutation
  • cardEditorwhile open
Components read a controller's signals through the adapter. Controllers import no component, so they run and test on their own.

One feature in three files ​

A counter is the smallest thing Olas can build. A full screen, with queries, forms and child controllers, follows the same three steps.

Write the logic ​

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.

ts
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),
  }
})

Draw it ​

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.

Show the code for
tsx
import { useRoot, useValue } from '@kontsedal/olas-react'

export function Counter() {
  const { count, increment } = useRoot()
  return <button onClick={increment}>Clicked {useValue(count)} times</button>
}

Test it without a renderer ​

The test calls the controller the way a component would. It runs in Node, with no DOM, no jsdom and no act.

ts
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)
})

What the core does ​

@kontsedal/olas-core is one package with no framework inside it. These are the parts a controller builds with.

Explicit lifetimes ​

Dispose a controller and everything it created goes with it: children, effects, queries and forms. There is no cleanup to forget.

Shared queries ​

Two controllers that ask for the same key share one cache entry and one fetch. Stale times, retries, cancellation and pagination are built in.

Mutations that roll back ​

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.

Forms as signals ​

Fields, forms and field arrays, with sync and async validators, server errors and a submit that returns a typed result.

Server rendering ​

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.

Plugins ​

One contract for behavior that cuts across every query. Cross-tab sync, persistence, entity normalization and the offline queue are plugins on it.

Add only what you need ​

Every package is optional apart from the core and one adapter. Each one installs on its own.

Frameworks ​

  • olas-react Hooks for React 18 and later, and for Preact
  • olas-vue Signals as read-only refs, for Vue 3.4 and later
  • olas-svelte Stores for Svelte 4 and 5

Routing and forms ​

  • olas-router Route params and search as signals in controllers
  • olas-zod A Zod schema as the shape and validation of a form

Data and sync ​

Tools ​

Next, Getting started builds a todo list with a query, a view and a test.

Released under the MIT License.