Skip to content

Commit

Permalink
UseElmish with dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
alfonsogarciacaro committed Oct 28, 2021
1 parent 8ac147d commit 133e997
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 50 deletions.
118 changes: 71 additions & 47 deletions Feliz.UseElmish/UseElmish.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,89 @@ namespace Feliz.UseElmish
open Feliz
open Elmish

type private ElmishObservable<'State, 'Msg>() =
let mutable state: 'State option = None
let mutable listener: ('State -> unit) option = None
let mutable dispatcher: ('Msg -> unit) option = None

member _.Value = state

member _.SetState (model: 'State) (dispatch: 'Msg -> unit) =
state <- Some model
dispatcher <- Some dispatch
match listener with
| None -> ()
| Some listener -> listener model

member _.Dispatch(msg) =
match dispatcher with
| None -> () // Error?
| Some dispatch -> dispatch msg

member _.Subscribe(f) =
match listener with
| Some _ -> ()
| None -> listener <- Some f

[<AutoOpen>]
module UseElmishExtensions =
type private ElmishObservable<'Model, 'Msg>() =
let mutable hasDisposedOnce = false
let mutable state: 'Model option = None
let mutable listener: ('Model -> unit) option = None
let mutable dispatcher: ('Msg -> unit) option = None

member _.Value = state
member _.HasDisposedOnce = hasDisposedOnce

member _.SetState (model: 'Model) (dispatch: 'Msg -> unit) =
state <- Some model
dispatcher <- Some dispatch
match listener with
| None -> ()
| Some listener -> listener model

member _.Dispatch(msg) =
match dispatcher with
| None -> () // Error?
| Some dispatch -> dispatch msg

member _.Subscribe(f) =
match listener with
| Some _ -> ()
| None -> listener <- Some f

/// Disposes state (and dispatcher) but keeps subscription
member _.DisposeState() =
match state with
| Some state ->
match box state with
| :? System.IDisposable as disp -> disp.Dispose()
| _ -> ()
| _ -> ()
dispatcher <- None
state <- None
hasDisposedOnce <- true

let private runProgram (program: unit -> Program<'Arg, 'Model, 'Msg, unit>) (arg: 'Arg) (obs: ElmishObservable<'Model, 'Msg>) () =
program()
|> Program.withSetState obs.SetState
|> Program.runWith arg

match obs.Value with
| None -> failwith "Elmish program has not initialized"
| Some v -> v

let disposeState (state: obj) =
match box state with
| :? System.IDisposable as disp -> disp.Dispose()
| _ -> ()

type React with
[<Hook>]
static member useElmish(program: unit -> Program<'arg, 'State, 'Msg, unit>, arg: 'arg) =
static member useElmish(program: unit -> Program<'Arg, 'Model, 'Msg, unit>, arg: 'Arg, ?dependencies: obj array) =
// Don't use useMemo here because React doesn't guarantee it won't recreate it again
let obs, _ = React.useState(fun () -> ElmishObservable())

let state, setState = React.useState(fun () ->
program()
|> Program.withSetState obs.SetState
|> Program.runWith arg
let obs, _ = React.useState(fun () -> ElmishObservable<'Model, 'Msg>())

match obs.Value with
| None -> failwith "Elmish program has not initialized"
| Some v -> v)
let state, setState = React.useState(runProgram program arg obs)

React.useEffectOnce(fun () ->
React.createDisposable(fun () ->
match box state with
| :? System.IDisposable as disp -> disp.Dispose()
| _ -> ()))
React.useEffect((fun () ->
if obs.HasDisposedOnce then
runProgram program arg obs () |> setState
React.createDisposable(obs.DisposeState)
), defaultArg dependencies [||])

obs.Subscribe(setState)
state, obs.Dispatch

[<Hook>]
static member useElmish(program: unit -> Program<unit, 'State, 'Msg, unit>) =
React.useElmish(program, ())
static member useElmish(program: unit -> Program<unit, 'Model, 'Msg, unit>, ?dependencies: obj array) =
React.useElmish(program, (), ?dependencies=dependencies)

static member useElmish(init, update, arg, ?dependencies: obj array) =
React.useElmish((fun () -> Program.mkProgram init update (fun _ _ -> ())), arg)
[<Hook>]
static member useElmish(init: 'Arg -> 'Model * Cmd<'Msg>, update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, arg: 'Arg, ?dependencies: obj array) =
React.useElmish((fun () -> Program.mkProgram init update (fun _ _ -> ())), arg, ?dependencies=dependencies)

static member useElmish(init, update, ?dependencies: obj array) =
React.useElmish(fun () -> Program.mkProgram init update (fun _ _ -> ()))
[<Hook>]
static member useElmish(init: unit -> 'Model * Cmd<'Msg>, update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, ?dependencies: obj array) =
React.useElmish((fun () -> Program.mkProgram init update (fun _ _ -> ())), ?dependencies=dependencies)

static member useElmish(initial, update, ?dependencies: obj array) =
React.useElmish(fun () -> Program.mkProgram (fun () -> initial) update (fun _ _ -> ()))
[<Hook>]
static member useElmish(init: 'Model * Cmd<'Msg>, update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, ?dependencies: obj array) =
React.useElmish((fun () -> Program.mkProgram (fun () -> init) update (fun _ _ -> ())), ?dependencies=dependencies)
69 changes: 66 additions & 3 deletions tests/Tests.fs
Original file line number Diff line number Diff line change
Expand Up @@ -561,27 +561,49 @@ module UseElmish =

type Msg =
| Increment
| IncrementAgain

let init = 0, Cmd.none

let update msg state =
match msg with
| Increment -> state + 1, Cmd.none
| IncrementAgain -> state + 1, Cmd.ofMsg Increment

let render = React.functionComponent(fun () ->
let state,dispatch = React.useElmish(init, update, [||])
let render = React.functionComponent(fun (props: {| subtitle: string |}) ->
let state, dispatch = React.useElmish(init, update, [|box props.subtitle|])

Html.div [
Html.h1 [
prop.testId "count"
prop.text state
]

Html.h2 props.subtitle

Html.button [
prop.text "Increment"
prop.onClick (fun _ -> dispatch Increment)
prop.testId "increment"
]

Html.button [
prop.text "Increment again"
prop.onClick (fun _ -> dispatch IncrementAgain)
prop.testId "increment-again"
]

])

let wrapper = React.functionComponent(fun () ->
let count, setCount = React.useState 0
Html.div [
Html.button [
prop.text "Increment wrapper"
prop.onClick (fun _ -> count + 1 |> setCount)
prop.testId "Increment-wrapper"
]
render {| subtitle = if count < 2 then "foo" else "bar" |}
])

let felizTests = testList "Feliz Tests" [
Expand Down Expand Up @@ -1101,7 +1123,7 @@ let felizTests = testList "Feliz Tests" [
}

testReactAsync "useElmish works" <| async {
let render = RTL.render(UseElmish.render())
let render = RTL.render(UseElmish.render {| subtitle = "foo" |})

Expect.equal (render.getByTestId("count").innerText) "0" "Should be initial state"

Expand All @@ -1112,6 +1134,47 @@ let felizTests = testList "Feliz Tests" [
Expect.equal (render.getByTestId("count").innerText) "1" "Should have been incremented"
|> Async.AwaitPromise
}

// See https://github.com/fable-compiler/fable-promise/issues/24#issuecomment-934328900
testReactAsync "useElmish works with commands" <| async {
let render = RTL.render(UseElmish.render {| subtitle = "foo" |})

Expect.equal (render.getByTestId("count").innerText) "0" "Should be initial state"

render.getByTestId("increment-again").click()

do!
RTL.waitFor <| fun () ->
Expect.equal (render.getByTestId("count").innerText) "2" "Should have been incremented twice"
|> Async.AwaitPromise
}

testReactAsync "useElmish works with dependencies" <| async {
let render = RTL.render(UseElmish.wrapper())

Expect.equal (render.getByTestId("count").innerText) "0" "Should be initial state"

render.getByTestId("increment").click()

do!
RTL.waitFor <| fun () ->
Expect.equal (render.getByTestId("count").innerText) "1" "Should have been incremented"
|> Async.AwaitPromise

render.getByTestId("increment-wrapper").click()

do!
RTL.waitFor <| fun () ->
Expect.equal (render.getByTestId("count").innerText) "1" "State should be same because dependency hasn't changed"
|> Async.AwaitPromise

render.getByTestId("increment-wrapper").click()

do!
RTL.waitFor <| fun () ->
Expect.equal (render.getByTestId("count").innerText) "0" "State should have been reset because dependency has changed"
|> Async.AwaitPromise
}
]

[<EntryPoint>]
Expand Down

0 comments on commit 133e997

Please sign in to comment.