Skip to main content
On this pageOverview

Scaling with Submodels

Overview

As your app grows, a single Model/Message/Update becomes unwieldy. The submodel pattern lets you split your app into self-contained modules, each with its own Model, Message, init, update, and view.

Submodule Structure

Each submodule has its own Model, Message, and update:

import { Match as M, Schema as S } from 'effect'
import { Command } from 'foldkit/command'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'

// MODEL

export const Model = S.Struct({
  theme: S.String,
  notifications: S.Boolean,
})

export type Model = typeof Model.Type

// MESSAGE

export const ChangedTheme = m('ChangedTheme', { theme: S.String })
export const Message = S.Union(ChangedTheme)
export type Message = typeof Message.Type

// UPDATE

export const update = (
  model: Model,
  message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
  M.value(message).pipe(
    M.tagsExhaustive({
      ChangedTheme: ({ theme }) => [
        evo(model, { theme: () => theme }),
        [],
      ],
    }),
  )

Parent Responsibilities

The parent model embeds the child model as a field:

import { Schema as S } from 'effect'

import * as Settings from './page/settings'

export const Model = S.Struct({
  username: S.String,
  settings: Settings.Model,
})

export type Model = typeof Model.Type

The parent has a wrapper message that contains the child message:

import { Schema as S } from 'effect'
import { m } from 'foldkit/message'

import * as Settings from './page/settings'

export const GotSettingsMessage = m('GotSettingsMessage', {
  message: Settings.Message,
})

export const Message = S.Union(GotSettingsMessage)
export type Message = typeof Message.Type

In update, delegate to the child and rewrap returned commands:

import { Array, Effect, Match as M } from 'effect'
import { Command } from 'foldkit/command'
import { evo } from 'foldkit/struct'

export const update = (
  model: Model,
  message: Message,
): [Model, ReadonlyArray<Command<Message>>] =>
  M.value(message).pipe(
    M.tagsExhaustive({
      GotSettingsMessage: ({ message }) => {
        const [nextSettings, commands] = Settings.update(
          model.settings,
          message,
        )

        const mappedCommands = Array.map(
          commands,
          Effect.map(message => GotSettingsMessage({ message })),
        )

        return [
          evo(model, { settings: () => nextSettings }),
          mappedCommands,
        ]
      },
    }),
  )

See the Shopping Cart example for a complete implementation of this pattern.