Skip to main content
On this pageThe First Model

Init & Flags

The First Model

init constructs the first Model and returns any Commands that should run when the application starts. It returns Update.Return<Model, Message>, the same type as update.

The counter starts at zero and has no startup work:

import { Schema } from 'effect'
import type { Runtime } from 'foldkit'
import { defineMessageUnion } from 'foldkit/message'

const Model = Schema.Struct({
  count: Schema.Number,
})
type Model = typeof Model.Type

const Message = defineMessageUnion({
  ClickedIncrement: {},
  ClickedDecrement: {},
})
type Message = typeof Message.Type

const init: Runtime.ApplicationInit<Model, Message> = () => ({
  model: { count: 0 },
})

A non-routing application or element calls init with no arguments. A routing application passes the current URL, so its first Model can reflect the route. When the application declares Flags, they become the first argument in either form.

Startup Data from Flags

Flags carry data from outside the application into init. Typical sources include persisted state, runtime configuration, and request-specific data supplied during server rendering.

Define the boundary with a Flags Schema. For a fresh client boot, also define an Effect that obtains a value matching that Schema:

import { Effect, Option, Schema } from 'effect'
import { KeyValueStore } from 'effect/unstable/persistence'

import { BrowserKeyValueStore } from '@effect/platform-browser'

const Todo = Schema.Struct({
  id: Schema.String,
  text: Schema.String,
  completed: Schema.Boolean,
})

const Todos = Schema.Array(Todo)

const TodosJsonString = Schema.fromJsonString(Schema.toCodecJson(Todos))

const Flags = Schema.Struct({
  todos: Schema.Option(Todos),
})
type Flags = typeof Flags.Type

const flags: Effect.Effect<Flags> = Effect.gen(function* () {
  const store = yield* KeyValueStore.KeyValueStore
  const todosJson = yield* Effect.fromOption(
    Option.fromNullishOr(yield* store.get('todos')),
  )

  const decodeTodos = Schema.decodeEffect(TodosJsonString)
  const todos = yield* decodeTodos(todosJson)

  return Flags.make({ todos: Option.some(todos) })
}).pipe(
  Effect.catch(() => Effect.succeed(Flags.make({ todos: Option.none() }))),
  Effect.provide(BrowserKeyValueStore.layerLocalStorage),
)

init receives the decoded Flags value and folds it into the first Model:

import { Option, Schema } from 'effect'
import type { Runtime } from 'foldkit'
import { defineMessageUnion } from 'foldkit/message'

const Model = Schema.Struct({
  count: Schema.Number,
  startingCount: Schema.Option(Schema.Number),
})
type Model = typeof Model.Type

const Flags = Schema.Struct({
  savedCount: Schema.Option(Schema.Number),
})
type Flags = typeof Flags.Type

const Message = defineMessageUnion({
  ClickedIncrement: {},
})
type Message = typeof Message.Type

const init: Runtime.ApplicationInit<Model, Message, Flags> = flags => ({
  model: {
    count: Option.getOrElse(flags.savedCount, () => 0),
    startingCount: flags.savedCount,
  },
})

Fresh Client Boot

Pass the Schema to Runtime.makeApplication as Flags, then pass the Effect to Runtime.run. The runtime resolves the Effect before calling init. If the configuration omits the Schema, init takes no Flags argument and the compiler rejects mismatched wiring.

import { Runtime } from 'foldkit'

import { Flags, Model, flags, init, update, view } from './main'

const application = Runtime.makeApplication({
  Model,
  init,
  update,
  view,
  Flags,
  container: document.getElementById('root'),
})

Runtime.run(application, { flags })

The example provides KeyValueStore inside the Flags Effect because that service is used only during startup. If the same singleton is also needed by Commands or Subscriptions, leave the requirement in the Effect type and provide it through the application's resources Layer. The runtime builds that Layer once and shares it. See Resources for the full setup.

Server Rendering and Hydration

Server rendering provides Flags from the request or build instead of running a client Flags Effect. renderToString uses that value to call init, encodes it through the Schema, and embeds the result in the HTML. Runtime.hydrate decodes the same value and calls the same init, so the client reconstructs the Model that produced the server HTML.

A hydrating entry does not provide a client Flags Effect. Missing or invalid handoff data fails startup instead of silently booting a different Model. The Server Rendering guide explains which data is safe and reproducible across that boundary.

Once one Model, Message union, and update function become too large to reason about as a unit, decompose the state machine into Submodels. Each child owns its own Model, Messages, update, and Commands behind an explicit parent boundary.