Header menu logo Mibo

Subscriptions

Subscriptions connect external event sources to your Elmish update loop. Unlike commands (one-time effects), subscriptions run continuously, dispatching messages whenever events occur.

Quick Start

open Mibo.Elmish

// Define subscription IDs
type SubId = 
  static member Keyboard = SubId.ofString "keyboard"
  static member Network = SubId.ofString "network"

// Create a subscription
let keyboardSub : Sub<Msg> =
  SubId.Keyboard,
  fun dispatch ->
    // Listen to events, dispatch messages
    let handler = EventHandler<KeyboardEvent>(fun _ e ->
      dispatch (KeyPressed e.Key)
    )
    Keyboard.addListener handler
    
    // Return disposable to clean up
    { new IDisposable with
      member _.Dispose() = Keyboard.removeListener handler
    }

// In your program
Program.mkProgram init update
|> Program.withSubscription (fun _model -> keyboardSub)

How Subscriptions Work

The Elmish runtime diffs subscriptions by SubId each frame:

This gives you precise control over subscription lifetimes based on your model state.

Creating Subscriptions

Basic Subscription

let timerSub (interval: TimeSpan) : Sub<Msg> =
  let id = SubId.ofString "timer"
  
  id,
  fun dispatch ->
    let timer = new Timer(interval)
    timer.Elapsed.Add(fun _ -> dispatch Tick)
    timer.Start()
    
    { new IDisposable with
      member _.Dispose() = timer.Dispose()
    }

Conditional Subscriptions

Start/stop based on model state:

let subscribe model =
  if model.IsConnected then
    Sub.batch2 (
      heartbeatSub,
      messageListenerSub
    )
  else
    Sub.none

Multiple Subscriptions

Function

Use Case

Sub.batch [sub1; sub2]

Variable list

Sub.batch2 (a, b)

Exactly 2 (optimized)

Sub.batch3 (a, b, c)

Exactly 3 (optimized)

Sub.batch4 (a, b, c, d)

Exactly 4 (optimized)

Subscription IDs

IDs must be unique per subscription. Use namespacing for parent-child composition:

module Player =
  let inputSub : Sub<Player.Msg> =
    SubId.ofString "input",
    fun dispatch -> ...

// Parent prefixes child IDs:
let parentSub =
  Player.inputSub |> Sub.map "player" PlayerMsg
// Resulting ID: "player/input"

Parent-Child Composition

Child modules often need their own subscriptions:

module Chat =
  type Msg = NewMessage of string | ConnectionLost
  
  let subscribe (model: Chat.Model) : Sub<Chat.Msg> =
    if model.IsOpen then
      SubId.ofString "chat/socket",
      fun dispatch ->
        let ws = new WebSocket("ws://server/chat")
        ws.OnMessage.Add(fun e -> dispatch (NewMessage e.Data))
        ws
    else
      Sub.none

// Parent wires it up:
type Parent.Msg = ChatMsg of Chat.Msg

let subscribe model =
  model.Chat
  |> Chat.subscribe
  |> Sub.map "chat" ChatMsg  // Prefix: "chat/chat/socket"

Common Patterns

Input Handling

For continuous input (not events):

let inputSub : Sub<Msg> =
  SubId.ofString "input",
  fun dispatch ->
    // Mibo provides this via Input.subscribe
    Input.subscribe (fun actions ->
      dispatch (InputChanged actions)
    )

Note: Mibo's built-in input system handles this for you. See Input.

Network Events

let networkSub (client: NetworkClient) : Sub<Msg> =
  SubId.ofString "network",
  fun dispatch ->
    let handler = client.OnPacket.Subscribe(fun packet ->
      dispatch (PacketReceived packet)
    )
    handler

Time-based

// Every second, dispatch a tick
let fpsSub : Sub<Msg> =
  SubId.ofString "fps",
  fun dispatch ->
    let rec loop () = async {
      do! Async.Sleep 1000
      dispatch CalculateFps
      return! loop()
    }
    let cts = new CancellationTokenSource()
    Async.Start(loop(), cts.Token)
    
    { new IDisposable with
      member _.Dispose() = cts.Cancel()
    }

Lifecycle Management

The runtime automatically manages subscription lifecycles:

// Frame 1: Model says we need network
let subscribe model =
  if model.Online then networkSub else Sub.none
// Runtime: Starts networkSub

// Frame 2: Model goes offline
// Runtime: Disposes networkSub (ID disappeared)

// Frame 3: Model back online
// Runtime: Starts fresh networkSub

Clean up resources in your disposable:

fun dispatch ->
  let resource = acquireResource()
  
  { new IDisposable with
    member _.Dispose() =
      resource.Close()
      resource.Dispose()
  }

Performance Notes

See Also

type SubId = static member Keyboard: obj with get static member Network: obj with get
val keyboardSub: obj * (obj -> obj)
property SubId.Keyboard: obj with get
val dispatch: obj
val handler: obj
val timerSub: interval: 'a -> 'b * ('c -> 'd)
val interval: 'a
val id: 'b
val dispatch: 'c
val timer: obj
val subscribe: model: 'a -> 'b
val model: 'a
val inputSub: obj * (obj -> obj)
val parentSub: obj
module Player from subscriptions
type Msg = | NewMessage of string | ConnectionLost
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
val subscribe: model: 'a -> 'b * ('c -> 'd)
val ws: 'd
union case Msg.NewMessage: string -> Msg
module Chat from subscriptions
val networkSub: client: 'a -> 'b * ('c -> 'd)
val client: 'a
val handler: 'd
val fpsSub: obj * ((obj -> unit) -> obj)
val dispatch: (obj -> unit)
val loop: unit -> Async<'a>
val async: AsyncBuilder
Multiple items
type Async = static member AsBeginEnd: computation: ('Arg -> Async<'T>) -> ('Arg * AsyncCallback * objnull -> IAsyncResult) * (IAsyncResult -> 'T) * (IAsyncResult -> unit) static member AwaitEvent: event: IEvent<'Del,'T> * ?cancelAction: (unit -> unit) -> Async<'T> (requires delegate and 'Del :> Delegate) static member AwaitIAsyncResult: iar: IAsyncResult * ?millisecondsTimeout: int -> Async<bool> static member AwaitTask: task: Task<'T> -> Async<'T> + 1 overload static member AwaitWaitHandle: waitHandle: WaitHandle * ?millisecondsTimeout: int -> Async<bool> static member CancelDefaultToken: unit -> unit static member Catch: computation: Async<'T> -> Async<Choice<'T,exn>> static member Choice: computations: Async<'T option> seq -> Async<'T option> static member FromBeginEnd: beginAction: (AsyncCallback * objnull -> IAsyncResult) * endAction: (IAsyncResult -> 'T) * ?cancelAction: (unit -> unit) -> Async<'T> + 3 overloads static member FromContinuations: callback: (('T -> unit) * (exn -> unit) * (OperationCanceledException -> unit) -> unit) -> Async<'T> ...

--------------------
type Async<'T>
static member Async.Sleep: dueTime: System.TimeSpan -> Async<unit>
static member Async.Sleep: millisecondsDueTime: int -> Async<unit>
val cts: obj
static member Async.Start: computation: Async<unit> * ?cancellationToken: System.Threading.CancellationToken -> unit
val subscribe: model: 'a -> ('b -> 'c * ('d -> 'e))
val dispatch: 'a
val resource: obj

Type something to start searching.