From a973d7f0b421aef0baa003f9bb7428d753dc2ae7 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Fri, 9 Aug 2024 16:26:32 +0300 Subject: [PATCH 1/5] refactor: move hooks from type to module - Introduce `Hooks` module with all the hooks moved there instead of being defined in the `React` type. This way, the generated JS is cleaner because module function names aren't mangled. - Move `React` inline type extensions to a separate "Extensions" file. - Remove `Hook` attributes from inline functions for their irrelevance. --- src/ElmishStore/ElmishStore.fsproj | 1 + src/ElmishStore/Extensions.fs | 23 +++++++++ src/ElmishStore/Hooks.fs | 66 ++++++++++++------------- src/ElmishStore/UseSyncExternalStore.fs | 1 - 4 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 src/ElmishStore/Extensions.fs diff --git a/src/ElmishStore/ElmishStore.fsproj b/src/ElmishStore/ElmishStore.fsproj index 19c0958..2f9202f 100644 --- a/src/ElmishStore/ElmishStore.fsproj +++ b/src/ElmishStore/ElmishStore.fsproj @@ -22,6 +22,7 @@ + diff --git a/src/ElmishStore/Extensions.fs b/src/ElmishStore/Extensions.fs new file mode 100644 index 0000000..e24c5b5 --- /dev/null +++ b/src/ElmishStore/Extensions.fs @@ -0,0 +1,23 @@ +namespace ElmishStore + +open Fable.Core +open Feliz +open ElmishStore + +[] +type React = + + /// Provides a current snapshot of the store's state selected by the selector function. + /// NOTE: Selector returning value needs to be referentially stable. + static member inline useElmishStore(store, selector: 'model -> 'a) = + Hooks.useElmishStore store selector + + /// Provides a current snapshot of the store's state selected by the selector function. + /// The result of the selector function is memoized and compared with isEqual function. + static member inline useElmishStoreMemoized(store, selector: 'model -> 'a, isEqual) = + Hooks.useElmishStoreMemoizedWithCustomEquality store selector isEqual + + /// Provides a current snapshot of the store's state selected by the selector function. + /// The result of the selector function is memoized and compared with structural equality. + static member inline useElmishStoreMemoized(store, selector: 'model -> 'a) = + Hooks.useElmishStoreMemoized store selector \ No newline at end of file diff --git a/src/ElmishStore/Hooks.fs b/src/ElmishStore/Hooks.fs index f904da6..fa4aab5 100644 --- a/src/ElmishStore/Hooks.fs +++ b/src/ElmishStore/Hooks.fs @@ -1,49 +1,47 @@ -namespace ElmishStore +module ElmishStore.Hooks open Fable.Core open Feliz open ElmishStore -[] -type React = - /// Provides a current snapshot of the store's state selected by the selector function. - /// NOTE: Selector returning value needs to be referentially stable. - [] - static member useElmishStore(store, selector: 'model -> 'a) = +/// Provides a current snapshot of the store's state selected by the selector function. +/// NOTE: Selector returning value needs to be referentially stable. +[] +let useElmishStore store (selector: 'model -> 'a) = ReactBindings.useSyncExternalStore ( - store.Subscribe, - React.useCallback ( - (fun () -> store.GetModel() |> selector), - [| box store; box selector |] - ) + store.Subscribe, + React.useCallback ( + (fun () -> store.GetModel() |> selector), + [| box store; box selector |] + ) ) - /// Provides a current snapshot of the store's state selected by the selector function. - /// The result of the selector function is memoized and compared with isEqual function. - [] - static member useElmishStoreMemoized(store, selector: 'model -> 'a, isEqual) = +/// Provides a current snapshot of the store's state selected by the selector function. +/// The result of the selector function is memoized and compared with isEqual function. +[] +let useElmishStoreMemoizedWithCustomEquality store (selector: 'model -> 'a) isEqual = ReactBindings.useSyncExternalStoreWithSelector ( - store.Subscribe, - React.useCallback( - (fun () -> store.GetModel()), - [| box store; box selector |] - ), - selector, - isEqual + store.Subscribe, + React.useCallback( + (fun () -> store.GetModel()), + [| box store; box selector |] + ), + selector, + isEqual ) - /// Provides a current snapshot of the store's state selected by the selector function. - /// The result of the selector function is memoized and compared with structural equality. - [] - static member useElmishStoreMemoized(store, selector: 'model -> 'a) = +/// Provides a current snapshot of the store's state selected by the selector function. +/// The result of the selector function is memoized and compared with structural equality. +[] +let useElmishStoreMemoized store (selector: 'model -> 'a) = ReactBindings.useSyncExternalStoreWithSelector ( - store.Subscribe, - React.useCallback( - (fun () -> store.GetModel()), - [| box store; box selector |] - ), - selector, - (=) + store.Subscribe, + React.useCallback( + (fun () -> store.GetModel()), + [| box store; box selector |] + ), + selector, + (=) ) diff --git a/src/ElmishStore/UseSyncExternalStore.fs b/src/ElmishStore/UseSyncExternalStore.fs index 2322eec..d375758 100644 --- a/src/ElmishStore/UseSyncExternalStore.fs +++ b/src/ElmishStore/UseSyncExternalStore.fs @@ -27,7 +27,6 @@ type internal ReactBindings = ) : 'a = jsNative - [] static member inline useSyncExternalStoreWithSelector ( subscribe: UseSyncExternalStoreSubscribe, From 9be2b445741c7dfa8c80d421a5938943d6b74857 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Fri, 9 Aug 2024 16:30:41 +0300 Subject: [PATCH 2/5] build: update project configuration - Target .NET 6 instead of .NET 8 to relax package requirements. - Relax "FSharp.Core" dependency to v6. - Introduce "Fable.Package.SDK" package to facilitate project configuration for Fable. - Bump package version to 0.2.1. - [Example] Update "Feliz" to v2.8.0, update Fable to v4.7.0. --- .config/dotnet-tools.json | 2 +- .../ElmishStore.Example.fsproj | 1 + src/ElmishStore/ElmishStore.fsproj | 73 ++++++++++--------- 3 files changed, 41 insertions(+), 35 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 00e49ba..0d4a290 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "fable": { - "version": "4.9.0", + "version": "4.7.0", "commands": [ "fable" ] diff --git a/src/ElmishStore.Example/ElmishStore.Example.fsproj b/src/ElmishStore.Example/ElmishStore.Example.fsproj index 7efec8c..886fe66 100644 --- a/src/ElmishStore.Example/ElmishStore.Example.fsproj +++ b/src/ElmishStore.Example/ElmishStore.Example.fsproj @@ -31,6 +31,7 @@ + diff --git a/src/ElmishStore/ElmishStore.fsproj b/src/ElmishStore/ElmishStore.fsproj index 2f9202f..27fa7e4 100644 --- a/src/ElmishStore/ElmishStore.fsproj +++ b/src/ElmishStore/ElmishStore.fsproj @@ -1,36 +1,41 @@ - + - - Elmish.Store - A library that merges Elmish and React, providing an external store with efficient, selective component rendering capabilities. - fsharp;fable;react;elmish - Łukasz Krzywizna - SelectView Data Solutions - 0.1.0 - net8.0 - readme.md - https://github.com/SelectViewData/elmish-store - git - MIT - - - - - - - - - - - - - - - - - - - - - + + Elmish.Store + A library that merges Elmish and React, providing an external store with efficient, selective component rendering capabilities. + fsharp;fable;react;elmish + Łukasz Krzywizna + SelectView Data Solutions + 0.2.1 + net6.0 + readme.md + https://github.com/SelectViewData/elmish-store + git + MIT + + + fable-javascript + library + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7b039dddc22c1ef8e054abd73d5a85e805cd10c8 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Sun, 25 Aug 2024 23:45:32 +0300 Subject: [PATCH 3/5] feat!: generalize store API - Introduce `ElmishStoresHost` type with a bunch of related interfaces, as a replacement of the `ElmishStore` module. This allows controlling creation of the "store root" (instead of hard-coding it in the module). - Replace `uniqueName` string parameter, for a store key with a `storeKey` parameter of a generic type. This allows using any type as a key, including `string`. --- .../ElmishStore.Example.fsproj | 4 +- src/ElmishStore.Example/ModelStore.fs | 15 +- src/ElmishStore/ElmishStore.fs | 351 ++++++++++++------ src/ElmishStore/ElmishStore.fsproj | 4 +- 4 files changed, 260 insertions(+), 114 deletions(-) diff --git a/src/ElmishStore.Example/ElmishStore.Example.fsproj b/src/ElmishStore.Example/ElmishStore.Example.fsproj index 886fe66..47d5555 100644 --- a/src/ElmishStore.Example/ElmishStore.Example.fsproj +++ b/src/ElmishStore.Example/ElmishStore.Example.fsproj @@ -1,4 +1,4 @@ - + net8.0 @@ -36,4 +36,4 @@ - \ No newline at end of file + diff --git a/src/ElmishStore.Example/ModelStore.fs b/src/ElmishStore.Example/ModelStore.fs index 5929837..f61b4fd 100644 --- a/src/ElmishStore.Example/ModelStore.fs +++ b/src/ElmishStore.Example/ModelStore.fs @@ -11,13 +11,16 @@ open Elmish.Debug #endif +let storesHost = ElmishStoresHost() + let store = - Program.mkProgram init update (fun _ _ -> ()) - #if DEBUG - |> Program.withConsoleTrace - |> Program.withDebugger - #endif - |> ElmishStore.createStore "main" + let program = + Program.mkProgram init update (fun _ _ -> ()) + #if DEBUG + |> Program.withConsoleTrace + |> Program.withDebugger + #endif + storesHost.create "main" program [] let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector) diff --git a/src/ElmishStore/ElmishStore.fs b/src/ElmishStore/ElmishStore.fs index 2abd430..3000cdc 100644 --- a/src/ElmishStore/ElmishStore.fs +++ b/src/ElmishStore/ElmishStore.fs @@ -6,134 +6,277 @@ open Fable.Core open ElmishStore open System.Collections.Generic -type ElmishStore<'model, 'msg> = { - GetModel: unit -> 'model - Dispatch: 'msg -> unit - Subscribe: UseSyncExternalStoreSubscribe +type ElmishStore<'Model, 'Msg> = { + GetModel: unit -> 'Model + Dispatch: 'Msg -> unit + Subscribe: UseSyncExternalStoreSubscribe } -type private StoreState<'arg, 'model, 'msg> = { - Store: ElmishStore<'model, 'msg> - SetTermination: bool -> unit +type private StoreState<'Arg, 'Model, 'Msg> = { + Store: ElmishStore<'Model, 'Msg> + SetTermination: bool -> unit } -module ElmishStore = +type internal StoreKey<'T when 'T : equality> = 'T - let mutable private stores: Dictionary = Dictionary() +type IElmishProgramBasedStoreInitializer<'StoreKey when StoreKey<'StoreKey>> = + abstract CreateStore : + program: Program * + storeKey: 'StoreKey -> + ElmishStore<'Model, 'Msg> + abstract CreateStoreWithArg : + program: Program<'Arg, 'Model, 'Msg, unit> * + storeKey: 'StoreKey * + arg: 'Arg -> + ElmishStore<'Model, 'Msg> - let private initiate - uniqueName - (arg: 'arg) - (program: Program<'arg, 'model, 'msg, unit>) - (getState: unit -> 'model option) - = +type IElmishFunctionsBasedStoreInitializer<'StoreKey when StoreKey<'StoreKey>> = + abstract CreateStoreWithFunctions : + init: (unit -> 'Model * Cmd<'Msg>) * + update: ('Msg -> 'Model -> 'Model * Cmd<'Msg>) * + storeKey: 'StoreKey -> + ElmishStore<'Model, 'Msg> + abstract CreateStoreWithFunctionsAndArg : + init: ('Arg -> 'Model * Cmd<'Msg>) * + update: ('Msg -> 'Model -> 'Model * Cmd<'Msg>) * + storeKey: 'StoreKey * + arg: 'Arg -> + ElmishStore<'Model, 'Msg> - let mutable state = getState () - let mutable finalDispatch = None - let mutable shouldTerminate = false +type IElmishStoreInitializer<'StoreKey when StoreKey<'StoreKey>> = + inherit IElmishFunctionsBasedStoreInitializer<'StoreKey> + inherit IElmishProgramBasedStoreInitializer<'StoreKey> - let setTermination should = shouldTerminate <- should +type IElmishStoreAccessor<'StoreKey when StoreKey<'StoreKey>> = + abstract GetStore<'Model, 'Msg when StoreKey<'StoreKey>> : + storeKey: 'StoreKey -> ElmishStore<'Model, 'Msg> + abstract TryGetStore<'Model, 'Msg when StoreKey<'StoreKey>> : + storeKey: 'StoreKey -> ElmishStore<'Model, 'Msg> option - let dispatch msg = - match finalDispatch with - | Some finalDispatch -> finalDispatch msg - | None -> failwith "You're using initial dispatch. That shouldn't happen." +type IElmishStoresHost<'StoreKey when StoreKey<'StoreKey>> = + inherit IElmishStoreInitializer<'StoreKey> + inherit IElmishStoreAccessor<'StoreKey> - let subscribers = ResizeArray unit>() - let subscribe callback = - subscribers.Add(callback) - fun () -> subscribers.Remove(callback) |> ignore +type ElmishStoresHost<'StoreKey when StoreKey<'StoreKey>>() = - let mapSetState setState model dispatch = - setState model dispatch - let oldModel = state - state <- Some model - finalDispatch <- Some dispatch - // Skip re-renders if model hasn't changed - if not (obj.ReferenceEquals(model, oldModel)) then - subscribers |> Seq.iter (fun callback -> callback ()) + let mutable stores: Dictionary<'StoreKey, obj> = Dictionary<'StoreKey, obj>() - let mapInit userInit arg = - if state.IsSome then state.Value, Cmd.none else userInit arg + let initiate + storeKey + (arg: 'Arg) + (program: Program<'Arg, 'Model, 'Msg, unit>) + (getState: unit -> 'Model option) + = + let mutable state = getState () + let mutable finalDispatch = None + let mutable shouldTerminate = false - let mapTermination (predicate, terminate) = - let pred msg = predicate msg || shouldTerminate - pred, terminate + let setTermination should = shouldTerminate <- should - program - |> Program.map mapInit id id mapSetState id mapTermination - |> Program.runWith arg + let dispatch msg = + match finalDispatch with + | Some finalDispatch -> finalDispatch msg + | None -> failwith "You're using initial dispatch. That shouldn't happen." - let getState () = - match state with - | Some state -> state - | None -> failwith "State is not initialized. That shouldn't happen." + let subscribers = ResizeArray unit>() - let store = { - GetModel = getState - Dispatch = dispatch - Subscribe = UseSyncExternalStoreSubscribe subscribe - } + let subscribe callback = + subscribers.Add(callback) + fun () -> subscribers.Remove(callback) |> ignore - let storeState = { - Store = store - SetTermination = setTermination - } + let mapSetState setState model dispatch = + setState model dispatch + let oldModel = state + state <- Some model + finalDispatch <- Some dispatch + // Skip re-renders if model hasn't changed + if not (obj.ReferenceEquals(model, oldModel)) then + subscribers |> Seq.iter (fun callback -> callback ()) - stores[uniqueName] <- box storeState - store + let mapInit userInit arg = + if state.IsSome then state.Value, Cmd.none else userInit arg - let createStoreWith uniqueName (arg: 'arg) (program: Program<'arg, 'model, 'msg, unit>) = + let mapTermination (predicate, terminate) = + let pred msg = predicate msg || shouldTerminate + pred, terminate - let getState = - if stores.ContainsKey(uniqueName) then - let storeState = stores[uniqueName] |> unbox> - storeState.SetTermination true - (fun () -> Some(storeState.Store.GetModel())) - else - (fun () -> None) + program + |> Program.map mapInit id id mapSetState id mapTermination + |> Program.runWith arg - initiate uniqueName arg program getState + let getState () = + match state with + | Some state -> state + | None -> failwith "State is not initialized. That shouldn't happen." + + let store = { + GetModel = getState + Dispatch = dispatch + Subscribe = UseSyncExternalStoreSubscribe subscribe + } + + let storeState = { + Store = store + SetTermination = setTermination + } + + stores[storeKey] <- box storeState + store + + let createStoreWith storeKey (arg: 'Arg) (program: Program<'Arg, 'Model, 'Msg, unit>) = + + let getState = + if stores.ContainsKey(storeKey) then + let storeState = stores[storeKey] |> unbox> + storeState.SetTermination true + (fun () -> Some(storeState.Store.GetModel())) + else + (fun () -> None) + + initiate storeKey arg program getState + + let createStore storeKey program : ElmishStore<'Model, 'Msg> = + createStoreWith storeKey () program + + interface IElmishStoresHost<'StoreKey> with + + member this.CreateStore(program: Program, storeKey: StoreKey<'StoreKey>) = + createStore storeKey program + + member this.CreateStoreWithArg(program: Program<'Arg, 'Model, 'Msg, unit>, storeKey, arg) = + createStoreWith storeKey arg program + + member this.CreateStoreWithFunctions( + init: unit -> 'Model * Cmd<'Msg>, + update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, + storeKey + ) = + let program = Program.mkProgram init update (fun _ _ -> ()) + (this :> IElmishStoresHost<'StoreKey>).CreateStore(program, storeKey) + + member this.CreateStoreWithFunctionsAndArg( + init: 'Arg -> 'Model * Cmd<'Msg>, + update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, + storeKey, + arg: 'Arg + ) = + let program = Program.mkProgram init update (fun _ _ -> ()) + (this :> IElmishStoresHost<'StoreKey>).CreateStoreWithArg(program, storeKey, arg) + + member this.GetStore<'Model, 'Msg>(storeKey) = + try + let storeState = + stores[storeKey] |> unbox> + storeState.Store + with + | :? KeyNotFoundException as e -> + failwithf + $"Elmish Store with key %A{storeKey} hasn't been found. Store has to be created before it can be accessed." + + member this.TryGetStore<'Model, 'Msg> storeKey = + match stores.TryGetValue storeKey with + | true, s -> + let storeState = unbox> s + Some storeState.Store + | false, _ -> None + + // The following functions have to be inlined because JS doesn't support methods overloading. + + member inline this.CreateStore(program: Program, storeKey) = + (this :> IElmishStoresHost<'StoreKey>).CreateStore(program, storeKey) + + member inline this.CreateStore(program: Program<'Arg, 'Model, 'Msg, unit>, storeKey, arg) = + (this :> IElmishStoresHost<'StoreKey>).CreateStoreWithArg(program, storeKey, arg) + + member inline this.CreateStore( + init: unit -> 'Model * Cmd<'Msg>, + update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, + storeKey + ) = + (this :> IElmishStoresHost<'StoreKey>).CreateStoreWithFunctions(init, update, storeKey) + + member inline this.CreateStore( + init: 'Arg -> 'Model * Cmd<'Msg>, + update: 'Msg -> 'Model -> 'Model * Cmd<'Msg>, + storeKey, + arg: 'Arg + ) = + let program = Program.mkProgram init update (fun _ _ -> ()) + (this :> IElmishStoresHost<'StoreKey>).CreateStoreWithArg(program, storeKey, arg) + + member inline this.GetStore storeKey = + (this :> IElmishStoreAccessor<'StoreKey>).GetStore storeKey + + member inline this.TryGetStore storeKey = + (this :> IElmishStoreAccessor<'StoreKey>).TryGetStore storeKey - let inline createStore uniqueName program : ElmishStore<'model, 'msg> = - createStoreWith uniqueName () program [] type ElmishStore = - static member Create - ( - program: Program<'arg, 'model, 'msg, unit>, - arg: 'arg, - uniqueName - ) : ElmishStore<'model, 'msg> = - ElmishStore.createStoreWith uniqueName arg program - - static member inline Create(program: Program, uniqueName) = - ElmishStore.Create(program, (), uniqueName) - - static member inline Create - ( - init: 'arg -> 'model * Cmd<'msg>, - update: 'msg -> 'model -> 'model * Cmd<'msg>, - arg: 'arg, - uniqueName - ) = - ElmishStore.Create((Program.mkProgram init update (fun _ _ -> ())), arg, uniqueName) - - static member inline Create - ( - init: unit -> 'model * Cmd<'msg>, - update: 'msg -> 'model -> 'model * Cmd<'msg>, - uniqueName - ) = - ElmishStore.Create(Program.mkProgram init update (fun _ _ -> ()), uniqueName) - - static member inline Create - ( - init: 'model * Cmd<'msg>, - update: 'msg -> 'model -> 'model * Cmd<'msg>, - uniqueName - ) = - ElmishStore.Create(Program.mkProgram (fun () -> init) update (fun _ _ -> ()), uniqueName) + static member inline create + (host: IElmishStoresHost<'StoreKey>) + storeKey + (program: Program) + = + host.CreateStore(program, storeKey) + + static member inline createWithArg + (host: IElmishStoresHost<'StoreKey>) + storeKey + arg + (program: Program<'Arg, 'Model, 'Msg, unit>) + = + host.CreateStoreWithArg(program, storeKey, arg) + + static member inline createWithFunctions + (host: IElmishStoresHost<'StoreKey>) + storeKey + (init: unit -> 'Model * Cmd<'Msg>) + update + = + host.CreateStoreWithFunctions(init, update, storeKey) + + static member inline createWithFunctionsAndArg + (host: IElmishStoresHost<'StoreKey>) + storeKey + arg + (init: 'Arg -> 'Model * Cmd<'Msg>) + update + = + host.CreateStoreWithFunctionsAndArg(init, update, storeKey, arg) + + +[] +module Extensions = + + type IElmishStoresHost<'StoreKey when StoreKey<'StoreKey>> with + + member inline this.create + storeKey + (program: Program) + = + this.CreateStore(program, storeKey) + + member inline this.createWithArg + storeKey + arg + (program: Program<'Arg, 'Model, 'Msg, unit>) + = + this.CreateStoreWithArg(program, storeKey, arg) + + member inline this.createWithFunctions + storeKey + (init: unit -> 'Model * Cmd<'Msg>) + update + = + this.CreateStoreWithFunctions(init, update, storeKey) + + member inline this.createWithFunctionsAndArg + storeKey + arg + (init: 'Arg -> 'Model * Cmd<'Msg>) + update + = + this.CreateStoreWithFunctionsAndArg(init, update, storeKey, arg) diff --git a/src/ElmishStore/ElmishStore.fsproj b/src/ElmishStore/ElmishStore.fsproj index 27fa7e4..f54a81e 100644 --- a/src/ElmishStore/ElmishStore.fsproj +++ b/src/ElmishStore/ElmishStore.fsproj @@ -6,7 +6,7 @@ fsharp;fable;react;elmish Łukasz Krzywizna SelectView Data Solutions - 0.2.1 + 0.3.0-beta.0 net6.0 readme.md https://github.com/SelectViewData/elmish-store @@ -38,4 +38,4 @@ - \ No newline at end of file + From bb6e10fd667a52b3cebed8cd609cba7e4e63b924 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Mon, 26 Aug 2024 14:03:53 +0300 Subject: [PATCH 4/5] feat: introduce `StoreApi` module `StoreApi` module defines: - `getElmishStoreApi` function that returns an object with `ElmishStore` and a set of selector hooks on that store. - `useElmishStoreApi` hook that just memoizes `getElmishStoreApi` result. --- src/ElmishStore.Example/ModelStore.fs | 7 +++++ src/ElmishStore/ElmishStore.fsproj | 3 +- src/ElmishStore/StoreApi.fs | 44 +++++++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/ElmishStore/StoreApi.fs diff --git a/src/ElmishStore.Example/ModelStore.fs b/src/ElmishStore.Example/ModelStore.fs index f61b4fd..05d9a5b 100644 --- a/src/ElmishStore.Example/ModelStore.fs +++ b/src/ElmishStore.Example/ModelStore.fs @@ -22,11 +22,18 @@ let store = #endif storesHost.create "main" program +(* [] let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector) [] let useSelectorMemoized (memoizedSelector: Model -> 'a) = React.useElmishStoreMemoized (store, memoizedSelector) +*) + +let storeApi = StoreApi.getStoreApi store + +let inline useSelector selector = storeApi.useSelector selector +let inline useSelectorMemoized selector = storeApi.useSelectorMemoized selector let dispatch = store.Dispatch \ No newline at end of file diff --git a/src/ElmishStore/ElmishStore.fsproj b/src/ElmishStore/ElmishStore.fsproj index f54a81e..10a0539 100644 --- a/src/ElmishStore/ElmishStore.fsproj +++ b/src/ElmishStore/ElmishStore.fsproj @@ -6,7 +6,7 @@ fsharp;fable;react;elmish Łukasz Krzywizna SelectView Data Solutions - 0.3.0-beta.0 + 0.3.0-beta.1 net6.0 readme.md https://github.com/SelectViewData/elmish-store @@ -26,6 +26,7 @@ + diff --git a/src/ElmishStore/StoreApi.fs b/src/ElmishStore/StoreApi.fs new file mode 100644 index 0000000..ac64c96 --- /dev/null +++ b/src/ElmishStore/StoreApi.fs @@ -0,0 +1,44 @@ +module ElmishStore.StoreApi + +open Feliz + +type IElmishStoreHooks<'Model> = + /// Provides a current snapshot of the store's state selected by the selector function. + /// NOTE: Selector returning value needs to be referentially stable. + abstract useSelector : selector: ('Model -> 'T) -> 'T + /// Provides a current snapshot of the store's state selected by the selector function. + /// The result of the selector function is memoized and compared with structural equality. + abstract useSelectorMemoized<'T when 'T : equality> : selector: ('Model -> 'T) -> 'T + /// Provides a current snapshot of the store's state selected by the selector function. + /// The result of the selector function is memoized and compared with isEqual function. + abstract useSelectorMemoizedWithCustomEquality : selector: ('Model -> 'T) -> isEqual: ('T -> 'T -> bool) -> 'T + +type IElmishStoreApi<'Model, 'Msg> = + inherit IElmishStoreHooks<'Model> + + abstract Store : ElmishStore<'Model, 'Msg> + + +let getElmishStoreApi (store: ElmishStore<'Model, 'Msg>) = + { new IElmishStoreApi<'Model, 'Msg> with + member _.Store = store + + member _.useSelector(selector: 'Model -> 'T): 'T = + Hooks.useElmishStore store selector + + member _.useSelectorMemoized<'T when 'T : equality> (selector: 'Model -> 'T) = + Hooks.useElmishStoreMemoized store selector + + member _.useSelectorMemoizedWithCustomEquality<'T> + (selector: 'Model -> 'T) + (isEqual: 'T -> 'T -> bool): 'T + = + Hooks.useElmishStoreMemoizedWithCustomEquality store selector isEqual + } + + +[] +let useElmishStoreApi (store: ElmishStore<'Model, 'Msg>) = + React.useMemo (fun () -> + getElmishStoreApi store + , [| store |]) From 77f86d45dca207e214d5b25f4b95fa8008b32f44 Mon Sep 17 00:00:00 2001 From: ArtemyB Date: Mon, 26 Aug 2024 14:52:40 +0300 Subject: [PATCH 5/5] fix(Example): fix new API call error, add some comments on the new API usage --- src/ElmishStore.Example/ModelStore.fs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/ElmishStore.Example/ModelStore.fs b/src/ElmishStore.Example/ModelStore.fs index 05d9a5b..87698a1 100644 --- a/src/ElmishStore.Example/ModelStore.fs +++ b/src/ElmishStore.Example/ModelStore.fs @@ -11,6 +11,8 @@ open Elmish.Debug #endif +// One additional step (comparing with the previous module-based API). +// However, creating the Host on the global level is essentially the same as using the module-based API. let storesHost = ElmishStoresHost() let store = @@ -22,17 +24,21 @@ let store = #endif storesHost.create "main" program -(* +(*// Previous API: [] let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector) [] let useSelectorMemoized (memoizedSelector: Model -> 'a) = - React.useElmishStoreMemoized (store, memoizedSelector) + React.useElmishStoreMemoized (store, memoizedSelector) *) -let storeApi = StoreApi.getStoreApi store +// Instead of defining separate custom hooks for a specific Store, +// just create all the Store-related hooks packed in an object. +let storeApi = StoreApi.getElmishStoreApi store +// These helpers should be inline so that Fable doesn't generate unnecessary extra-functions around the hooks. +// However, these helpers aren't really needed, as the hooks could be called directly from the `storeApi` value. let inline useSelector selector = storeApi.useSelector selector let inline useSelectorMemoized selector = storeApi.useSelectorMemoized selector