type State =
  {Count: int;}

Full name: index.State
State.Count: int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
type Msg =
  | Increment
  | Decrement

Full name: index.Msg
union case Msg.Increment: Msg
union case Msg.Decrement: Msg
val init : unit -> State

Full name: index.init
val update : msg:Msg -> state:State -> State

Full name: index.update
val msg : Msg
val state : State
val view : state:'a -> dispatch:'b -> 'c

Full name: index.view
val state : 'a
val dispatch : 'b
val sprintf : format:Printf.StringFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.sprintf
type Message =
  | Increment
  | Decrement
  | IncrementImmediate
  | IncrementDelayed

Full name: index.Message
union case Message.Increment: Message
union case Message.Decrement: Message
union case Message.IncrementImmediate: Message
union case Message.IncrementDelayed: Message
val update : msg:Message -> state:State -> State * 'a

Full name: index.update
val msg : Message
val nextState : State
val nextCmd : 'a
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
val update : msg:'a -> state:'b -> 'c * 'd

Full name: index.update
val msg : 'a
val state : 'b
val StartLoading : 'a
val nextState : 'c

Scaling Elmish Applications



Introduction to the techniques of breaking down large elmish programs into smaller ones



Zaid Ajaj - @zaid-ajaj

Table of content

  • Introduction
  • Basics of Elmish
  • Concept of an "Elmish program"
  • Importance of commands
  • Larger programs
  • Challenges of breaking down programs
    • Breaking down parent state
      • Keeping state of a single child program
      • Keeping state of all children programs
  • Child programs
    • State updates
    • dispatch
    • commands
  • Ongoing developement
  • Resources to learn more

Introduction

Basics of Elmish

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
type State = { Count: int }

type Msg = Increment | Decrement

let init() : State = { Count = 0 } 

let update msg state = 
    match msg with 
    | Increment -> { state with Count = state.Count + 1 }
    | Decrement -> { state with Count = state.Count - 1 }

let view state dispatch = 
    div [ ] [ 
        button [ OnClick (fun _ -> dispatch Increment) ]
               [ str "Increment" ]  
        button [ OnClick (fun _ -> dispatch Decrement) ]
               [ str "Decrement" ]
        h1 [ ] 
           [ str (sprintf "%d" state.Count) ] 
    ]

counter

Introducing Commands

  • Schedule async messages
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
type Message = 
    | Increment 
    | Decrement 
    | IncrementImmediate
    | IncrementDelayed 

// update : Message -> State -> State * Cmd<Message>
let update msg state  = 
    match msg with 
    | Increment -> 
        let nextState = { state with Count = state.Count + 1 }
        nextState, Cmd.none 

    | Decrement ->
        let nextState = { state with Count = state.Count - 1 }
        nextState, Cmd.none

    | IncrementImmediate ->
        let nextCmd = Cmd.ofMsg Increment 
        state, nextCmd

    | IncrementDelayed -> 
        let nextCmd = Cmd.afterTimeout 1000 Increment 
        state, nextCmd

counter

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
type State = 
    | Initial
    | Loading 
    | LoadedData of message:string 

type Message = 
    | StartLoading
    | ReceivedLoadedData of message:string
    | Reset

let update msg state = 
    match msg with 
    | StartLoading -> 
        let nextState = State.Loading
        nextState, Cmd.afterTimeout 1000 (ReceivedLoadedData "Your data is here")

    | ReceivedLoadedData receivedMessage ->
        let nextState = State.LoadedData receivedMessage
        nextState, Cmd.none

    | Reset -> 
        let nextState = State.Initial
        nextState, Cmd.none
 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
let view state dispatch = 
    match state with 
    | State.Initial -> 
        makeButton (fun _ -> dispatch StartLoading) "Start Loading"
    
    | State.Loading -> 
        spinner 
    
    | State.LoadedData message -> 
        div [ ] 
            [ showMessage message
              makeButton (fun _ -> dispatch Reset) "Reset" ] 

loader

Loading data from web api

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
| LoadDrafts -> 
    let nextState = { state with Drafts = Loading }
    
    let loadDraftsCmd =
        Cmd.fromAsync { 
            Value = Server.api.getDrafts (SecurityToken(authToken))
            Error = fun ex -> DraftsLoadingError "Network error occured"
            Success = function 
              | Ok drafts -> DraftsLoaded drafts
              | Error authError -> AuthenticationError "User was unauthorized"
         }

    nextState, loadDraftsCmd

| DraftsLoaded draftsFromServer -> 
    let nextState = { state with Drafts = Content draftsFromServer }
    nextState, Cmd.none

| DraftsLoadingError errorMessage -> 
    let nextState = { state with Drafts = LoadError errorMessage }
    nextState, Cmd.none

Combining the two programs

app

Start with spaghetti

  • Divide and conquer
  • Demo time!

Data communication and updates

parent-child

Where to are messages dispatched?

runtime

Division Patterns

  • Keep state of all children
  • Keep state a single child

Recommended Directory Structure

Design Principles

  • Child components don't know anything about their parents
  • Child components don't know anything about their siblings
  • Parent components manage child states and communication between children

Pro

  • End up with modular application
  • isolated components are easier to reason with
  • isolated components cannot change state outside of their "program"

Cons

  • Explicit wiring of children with their parents
  • Explicit data communication between children

But components are not always isolated!!!

  • Communication between sibling components
  • "Intercept and propagate" pattern
  • Make distinction between "internal" and "external" messages

Ongoing developement

  • Fable & Fable.React bindings
  • Documentation
  • Other platforms (Electron, React Native, Node.js, Cordova)
  • Your help is needed: PRs are always welcome

Resources to learn more

Starter templates