On this pageOverview
OutMessage
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.
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.TypeThe 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.
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.