Skip to main content
On this pageOverview

Model as Union

Overview

When your app has mutually exclusive states—like logged in vs logged out, wizard steps, or game phases—you can model your root state as a union of variants rather than embedding submodels in a struct.

Define each variant as a tagged struct, then combine them with S.Union:

import { Schema as S } from 'effect'
import { ts } from 'foldkit/schema'

const LoggedOut = ts('LoggedOut', {
  email: S.String,
  password: S.String,
})

const LoggedIn = ts('LoggedIn', {
  userId: S.String,
  username: S.String,
})

export const Model = S.Union(LoggedOut, LoggedIn)

export type Model = typeof Model.Type

In the view, use Match.tagsExhaustive to handle each variant:

import { Match as M } from 'effect'

export const view = (model: Model) =>
  M.value(model).pipe(
    M.tagsExhaustive({
      LoggedOut: renderLoginForm,
      LoggedIn: renderDashboard,
    }),
  )

To transition between states, return a different variant from update:

import { Match as M } from 'effect'
import { Command } from 'foldkit/command'

export const update = (
  model: Model,
  message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
  M.value(message).pipe(
    M.tagsExhaustive({
      ClickedLogin: () => [
        LoggedIn({ userId: '123', username: 'alice' }),
        [],
      ],
      ClickedLogout: () => [
        LoggedOut({ email: '', password: '' }),
        [],
      ],
    }),
  )

See the Auth example for a complete implementation.

If you need shared state across union variants, wrap the union in a struct:

import { Schema as S } from 'effect'

export const Model = S.Struct({
  theme: S.String,
  authState: S.Union(LoggedOut, LoggedIn),
})

export type Model = typeof Model.Type