Skip to main content
On this pageOverview

OutMessage

Overview

Your login Submodel has authenticated the user. Now what? The child can’t transition the root Model to a logged-in state because it only knows about its own Model. And it shouldn’t know about the root Model — that would break the encapsulation that makes Submodels useful in the first place.

The OutMessage pattern solves this. The child emits a semantic event — “login succeeded, here’s the session.” The parent decides what to do with it. The child describes what happened; the parent decides the consequences.

Compare to React

In React, you’d pass an onLoginSuccess callback as a prop. This works but couples the child to the parent’s interface. In Foldkit, OutMessage keeps the boundary clean — the child emits facts, the parent interprets them.

Defining OutMessages

OutMessages live alongside the child’s Message and follow the same naming conventions — past-tense facts describing what happened. SucceededLogin, not TransitionToLoggedIn. RequestedLogout, not DoLogout. The child doesn’t know or care what the parent does with the information.

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

// MESSAGE

export const SubmittedLoginForm = m('SubmittedLoginForm')

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

// OUT MESSAGE

export const SucceededLogin = m('SucceededLogin', {
  sessionId: S.String,
})

export const OutMessage = S.Union(SucceededLogin)
export type OutMessage = typeof OutMessage.Type

Emitting from the Child

The child’s update function returns a 3-tuple instead of the usual 2-tuple: Model, Commands, and an Option<OutMessage>. Most Messages return Option.none() — only the significant “I need to tell the parent something” moments return Option.some(...):

import { Match as M, Option } from 'effect'
import { Command } from 'foldkit'

export const update = (
  model: Model,
  message: Message,
): [
  Model,
  ReadonlyArray<Command.Command<Message>>,
  Option.Option<OutMessage>,
] =>
  M.value(message).pipe(
    M.tagsExhaustive({
      SubmittedLoginForm: () => [
        model,
        [login(model.email, model.password)],
        Option.none(),
      ],
      SucceededRequestLogin: ({ sessionId }) => [
        model,
        [],
        Option.some(SucceededLogin({ sessionId })),
      ],
    }),
  )

The Option makes the boundary explicit. SubmittedLoginForm fires a Command and returns Option.none() — nothing for the parent to act on yet. But when the login succeeds, the child emits Option.some(SucceededLogin({ sessionId })) — that’s the signal the parent needs.

Handling in the Parent

The parent uses Option.match on the OutMessage. onNone means the child handled it internally — just update the child’s slice of the Model. onSome means the child is surfacing something the parent needs to act on:

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

export const update = (
  model: Model,
  message: Message,
): readonly [Model, ReadonlyArray<Command.Command<Message>>] =>
  M.value(message).pipe(
    M.tagsExhaustive({
      GotLoginMessage: ({ message }) => {
        const [nextLogin, commands, maybeOutMessage] = Login.update(
          model.login,
          message,
        )

        const mappedCommands = Array.map(
          commands,
          Command.mapEffect(
            Effect.map(message => GotLoginMessage({ message })),
          ),
        )

        return Option.match(maybeOutMessage, {
          onNone: () => [
            evo(model, { login: () => nextLogin }),
            mappedCommands,
          ],
          onSome: outMessage =>
            M.value(outMessage).pipe(
              M.tagsExhaustive({
                SucceededLogin: ({ sessionId }) => [
                  LoggedIn({ sessionId }),
                  [...mappedCommands, saveSession(sessionId)],
                ],
              }),
            ),
        })
      },
    }),
  )

This is where the power of the pattern shows. When SucceededLogin arrives, the parent can do things the child has no knowledge of — transition to a completely different Model state, save the session, redirect the URL. The child stays focused on its domain; the parent handles cross-cutting concerns.

See the Auth example for a complete implementation: a login module emits SucceededLogin when authentication completes, and the parent transitions to the logged-in state, saves the session, and updates the URL — all triggered by a single OutMessage.

Stay in the update loop.

New releases, patterns, and the occasional deep dive.


Built with Foldkit.

© 2026 Devin Jameson