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
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) ]
]
|
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
|
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" ]
|
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
Start with spaghetti
- Divide and conquer
- Demo time!
Data communication and updates
Where to are messages dispatched?
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