On this pageOverview
Parent-Child Communication with OutMessage
Sometimes a child module needs to trigger a change in the parent's state. Child modules cannot directly update parent state—they only manage their own. The OutMessage pattern solves this—children emit semantic events that parents then handle.
Define OutMessage schemas alongside your child Message:
import { Schema as S } from 'effect'
import { m } from 'foldkit/message'
// MESSAGE
export const ClickedLogout = m('ClickedLogout')
export const Message = S.Union(ClickedLogout)
export type Message = typeof Message.Type
// OUT MESSAGE
export const RequestedLogout = m('RequestedLogout')
export const OutMessage = S.Union(RequestedLogout)
export type OutMessage = typeof OutMessage.TypeThe child update function returns a 3-tuple: model, commands, and an optional OutMessage:
import { Match as M, Option } from 'effect'
import { Command } from 'foldkit/command'
export const update = (
model: Model,
message: Message,
): [
Model,
ReadonlyArray<Command<Message>>,
Option.Option<OutMessage>,
] =>
M.value(message).pipe(
M.tagsExhaustive({
ClickedLogout: () => [
model,
[],
Option.some(RequestedLogout()),
],
}),
)The parent handles the OutMessage with Option.match, taking action when present:
import { Match as M, Option } from 'effect'
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, maybeOutMessage] =
Settings.update(model.settings, message)
return Option.match(maybeOutMessage, {
onNone: () => [
evo(model, { settings: () => nextSettings }),
commands,
],
onSome: outMessage =>
M.value(outMessage).pipe(
M.tagsExhaustive({
RequestedLogout: () => [
LoggedOut({ email: '', password: '' }),
[...commands, clearSession()],
],
}),
),
})
},
}),
)Use Array.map with Effect.map to wrap child commands in parent message types:
import { Array, Effect } from 'effect'
const [nextSettings, commands, maybeOutMessage] = Settings.update(
model.settings,
message,
)
const mappedCommands = Array.map(commands, command =>
Effect.map(command, message => GotSettingsMessage({ message })),
)OutMessages are semantic events (like LoginSucceeded) while commands are side effects. This separation keeps child modules focused on their domain while parents handle cross-cutting concerns. See the Auth example for a complete implementation.