diff --git a/Feliz.UseElmish/Feliz.UseElmish.fsproj b/Feliz.UseElmish/Feliz.UseElmish.fsproj
index 0578c83c..7e56ab2b 100644
--- a/Feliz.UseElmish/Feliz.UseElmish.fsproj
+++ b/Feliz.UseElmish/Feliz.UseElmish.fsproj
@@ -22,7 +22,6 @@
-
\ No newline at end of file
diff --git a/Feliz.UseElmish/UseElmish.fs b/Feliz.UseElmish/UseElmish.fs
index c5f400c3..e0c569e1 100644
--- a/Feliz.UseElmish/UseElmish.fs
+++ b/Feliz.UseElmish/UseElmish.fs
@@ -2,117 +2,90 @@ namespace Feliz.UseElmish
open Feliz
open Elmish
-open Fable.Core
-
-[]
-type internal RingState<'item> =
- | Writable of wx:'item array * ix:int
- | ReadWritable of rw:'item array * wix:int * rix:int
-
-type internal RingBuffer<'item>(size) =
- let doubleSize ix (items: 'item array) =
- seq { yield! items |> Seq.skip ix
- yield! items |> Seq.take ix
- for _ in 0..items.Length do
- yield Unchecked.defaultof<'item> }
- |> Array.ofSeq
-
- let mutable state : 'item RingState =
- Writable (Array.zeroCreate (max size 10), 0)
-
- member _.Pop() =
- match state with
- | ReadWritable (items, wix, rix) ->
- let rix' = (rix + 1) % items.Length
- match rix' = wix with
- | true ->
- state <- Writable(items, wix)
- | _ ->
- state <- ReadWritable(items, wix, rix')
- Some items.[rix]
- | _ ->
- None
-
- member _.Push (item:'item) =
- match state with
- | Writable (items, ix) ->
- items.[ix] <- item
- let wix = (ix + 1) % items.Length
- state <- ReadWritable(items, wix, ix)
- | ReadWritable (items, wix, rix) ->
- items.[wix] <- item
- let wix' = (wix + 1) % items.Length
- match wix' = rix with
- | true ->
- state <- ReadWritable(items |> doubleSize rix, items.Length, 0)
- | _ ->
- state <- ReadWritable(items, wix', rix)
[]
module UseElmishExtensions =
- let inline internal getDisposable (record: 'State) =
- match box record with
- | :? System.IDisposable as disposable -> Some disposable
- | _ -> None
+ 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
[]
- static member useElmish<'State,'Msg> (init: 'State * Cmd<'Msg>, update: 'Msg -> 'State -> 'State * Cmd<'Msg>, dependencies: obj[]) =
- let state = React.useRef(fst init)
- let ring = React.useRef(RingBuffer(10))
- let childState, setChildState = React.useState(fst init)
- let token = React.useCancellationToken()
- let setChildState () =
- JS.setTimeout(fun () ->
- if not token.current.IsCancellationRequested then
- setChildState state.current
- ) 0 |> ignore
-
- let rec dispatch (msg: 'Msg) =
- promise {
- let mutable nextMsg = Some msg
-
- while nextMsg.IsSome && not (token.current.IsCancellationRequested) do
- let msg = nextMsg.Value
- let (state', cmd') = update msg state.current
- cmd' |> List.iter (fun sub -> sub dispatch)
- nextMsg <- ring.current.Pop()
- state.current <- state'
- setChildState()
- }
- |> Promise.start
-
- let dispatch = React.useCallbackRef(dispatch)
+ 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<'Model, 'Msg>())
- React.useEffect((fun () ->
- React.createDisposable(fun () ->
- getDisposable state.current
- |> Option.iter (fun o -> o.Dispose())
- )
- ), dependencies)
+ let state, setState = React.useState(runProgram program arg obs)
React.useEffect((fun () ->
- state.current <- fst init
- setChildState()
-
- snd init
- |> List.iter (fun sub -> sub dispatch)
- ), dependencies)
+ if obs.HasDisposedOnce then
+ runProgram program arg obs () |> setState
+ React.createDisposable(obs.DisposeState)
+ ), defaultArg dependencies [||])
- React.useEffect(fun () -> ring.current.Pop() |> Option.iter dispatch)
-
- (childState, dispatch)
+ obs.Subscribe(setState)
+ state, obs.Dispatch
[]
- static member inline useElmish<'State,'Msg> (init: 'State * Cmd<'Msg>, update: 'Msg -> 'State -> 'State * Cmd<'Msg>) =
- React.useElmish(init, update, [||])
+ static member useElmish(program: unit -> Program, ?dependencies: obj array) =
+ React.useElmish(program, (), ?dependencies=dependencies)
[]
- static member useElmish<'State,'Msg> (init: unit -> 'State * Cmd<'Msg>, update: 'Msg -> 'State -> 'State * Cmd<'Msg>, dependencies: obj[]) =
- let init = React.useMemo(init, dependencies)
+ 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)
- React.useElmish(init, update, dependencies)
+ []
+ 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 inline useElmish<'State,'Msg> (init: unit -> 'State * Cmd<'Msg>, update: 'Msg -> 'State -> 'State * Cmd<'Msg>) =
- React.useElmish(init, update, [||])
+ 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)
diff --git a/docs/paket.references b/docs/paket.references
index 412fbbd8..7d9d8ae3 100644
--- a/docs/paket.references
+++ b/docs/paket.references
@@ -5,3 +5,4 @@ Fable.React
Fable.SimpleHttp
FSharp.Core
Zanaptak.TypedCssClasses
+Fable.Promise
\ No newline at end of file
diff --git a/paket.dependencies b/paket.dependencies
index 11597d4c..f14d2a56 100644
--- a/paket.dependencies
+++ b/paket.dependencies
@@ -9,6 +9,7 @@ group Main
nuget Fable.Mocha
nuget Fable.React
nuget Fable.SimpleHttp
+ nuget Fable.Promise >= 3.1
nuget FSharp.Core ~> 4.7.2
nuget Zanaptak.TypedCssClasses
diff --git a/paket.lock b/paket.lock
index 118348a3..fb7289e2 100644
--- a/paket.lock
+++ b/paket.lock
@@ -36,6 +36,9 @@ NUGET
Fable.Mocha (2.9.1)
Fable.Core (>= 3.0) - restriction: >= netstandard2.0
FSharp.Core (>= 4.7) - restriction: >= netstandard2.0
+ Fable.Promise (3.1)
+ Fable.Core (>= 3.1.5) - restriction: >= netstandard2.0
+ FSharp.Core (>= 4.7.2) - restriction: >= netstandard2.0
Fable.React (7.4)
Fable.Browser.Dom (>= 2.0.1) - restriction: >= netstandard2.0
Fable.Core (>= 3.1.5) - restriction: >= netstandard2.0
diff --git a/tests/Tests.fs b/tests/Tests.fs
index 175474a1..2b85f1ea 100644
--- a/tests/Tests.fs
+++ b/tests/Tests.fs
@@ -561,15 +561,17 @@ 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 [
@@ -577,11 +579,31 @@ module UseElmish =
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" [
@@ -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"
@@ -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
+ }
]
[]
diff --git a/tests/paket.references b/tests/paket.references
index 444ebd20..a8ed47f2 100644
--- a/tests/paket.references
+++ b/tests/paket.references
@@ -1,3 +1,4 @@
Fable.Browser.Dom
Fable.Mocha
FSharp.Core
+Fable.Promise