Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"isRoot": true,
"tools": {
"fable": {
"version": "4.9.0",
"version": "5.0.0",
"commands": [
"fable"
]
}
}
}
}
6 changes: 3 additions & 3 deletions Build.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
<Compile Include="Build.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fake.Core.Target" Version="6.0.0" />
<PackageReference Include="Fake.DotNet.Cli" Version="6.0.0" />
<PackageReference Include="Fake.IO.FileSystem" Version="6.0.0" />
<PackageReference Include="Fake.Core.Target" Version="6.1.4" />
<PackageReference Include="Fake.DotNet.Cli" Version="6.1.4" />
<PackageReference Include="Fake.IO.FileSystem" Version="6.1.4" />
</ItemGroup>
</Project>
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.201",
"rollForward": "latestMinor"
}
}
}
18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
{
"type": "module",
"private": true,
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"prestart": "dotnet tool restore",
"start": "dotnet fable watch ./src/ElmishStore.Example --outDir ./src/ElmishStore.Example/.fable-build --run vite"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"postcss": "^8.4.33",
"@tailwindcss/vite": "^4.2.2",
"@vitejs/plugin-react": "^6.0.1",
"remotedev": "^0.2.9",
"tailwindcss": "^3.4.1",
"vite": "^5.0.11"
"tailwindcss": "^4.2.2",
"vite": "^8.0.8"
},
"dependencies": {
"react": "^18.2.0",
"react-dom": "^18.2.0",
"use-sync-external-store": "^1.2.0"
"react": "^19.2.5",
"react-dom": "^19.2.5",
"use-sync-external-store": "^1.6.0"
}
}
6 changes: 0 additions & 6 deletions postcss.config.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/ElmishStore.Example/App.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ open Feliz

ReactDOM
.createRoot(Browser.Dom.document.getElementById "elmish-app")
.render (React.strictMode [ View.AppView() ])
.render (React.StrictMode [ View.AppView() ])
10 changes: 2 additions & 8 deletions src/ElmishStore.Example/ElmishStore.Example.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,9 @@
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Content Include="..\..\tailwind.config.js">
<Link>tailwind.config.js</Link>
</Content>
<Content Include="..\..\vite.config.js">
<Link>vite.config.js</Link>
</Content>
<Content Include="..\..\postcss.config.js">
<Link>postcss.config.js</Link>
</Content>
<Content Include="..\..\package.json">
<Link>package.json</Link>
</Content>
Expand All @@ -30,9 +24,9 @@
<EmbeddedResource Remove="public\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Fable.Elmish.Debugger" Version="4.0.0" />
<PackageReference Include="Fable.Elmish.Debugger" Version="4.2.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\ElmishStore\ElmishStore.fsproj" />
</ItemGroup>
</Project>
</Project>
9 changes: 5 additions & 4 deletions src/ElmishStore.Example/ModelStore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@ open Elmish.Debug

let store =
Program.mkProgram init update (fun _ _ -> ())
|> ElmishProgram.setup "main"
#if DEBUG
|> Program.withConsoleTrace
|> Program.withDebugger
|> ElmishProgram.mapProgram Program.withConsoleTrace
|> ElmishProgram.mapProgram Program.withDebugger
#endif
|> ElmishStore.createStore "main"
|> ElmishProgram.run

[<Hook>]
let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector)
Expand All @@ -26,4 +27,4 @@ let useSelector (selector: Model -> 'a) = React.useElmishStore (store, selector)
let useSelectorMemoized (memoizedSelector: Model -> 'a) =
React.useElmishStoreMemoized (store, memoizedSelector)

let dispatch = store.Dispatch
let dispatch = store.Dispatch
5 changes: 2 additions & 3 deletions src/ElmishStore.Example/styles/styles.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
@charset "utf-8";

@tailwind base;
@tailwind components;
@tailwind utilities;
@import "tailwindcss";
@source "../.fable-build";
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

explain that change

158 changes: 108 additions & 50 deletions src/ElmishStore/ElmishStore.fs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ namespace ElmishStore

open Elmish
open Fable.Core
open ElmishStore
open System.Collections.Generic

type ElmishStore<'model, 'msg> = {
Expand All @@ -12,61 +11,86 @@ type ElmishStore<'model, 'msg> = {
Subscribe: UseSyncExternalStoreSubscribe
}

type private StoreState<'arg, 'model, 'msg> = {
type private StoreState<'model, 'msg> = {
Store: ElmishStore<'model, 'msg>
SetTermination: bool -> unit
}

module ElmishStore =
type private RuntimeState<'model, 'msg> = {
UniqueName: string
mutable State: 'model option
mutable FinalDispatch: Dispatch<'msg> option
mutable ShouldTerminate: bool
Subscribers: ResizeArray<unit -> unit>
mutable PreviousStore: StoreState<'model, 'msg> option
}

let mutable private stores: Dictionary<string, obj> = Dictionary<string, obj>()
type ElmishProgramState<'arg, 'model, 'msg, 'view> = {
Program: Program<'arg, 'model, 'msg, 'view>
Run: Program<'arg, 'model, 'msg, 'view> -> 'arg -> ElmishStore<'model, 'msg>
}

let private initiate
uniqueName
(arg: 'arg)
(program: Program<'arg, 'model, 'msg, unit>)
(getState: unit -> 'model option)
=
module private Internal =

let mutable state = getState ()
let mutable finalDispatch = None
let mutable shouldTerminate = false
let mutable stores: Dictionary<string, obj> = Dictionary<string, obj>()

let setTermination should = shouldTerminate <- should
let tryGetStoreState (uniqueName: string) =
if stores.ContainsKey(uniqueName) then
Some(stores[uniqueName] |> unbox<StoreState<'model, 'msg>>)
else
None

module ElmishProgram =

let setup (uniqueName: string) (program: Program<'arg, 'model, 'msg, 'view>) =
let runtime = {
UniqueName = uniqueName
State = None
FinalDispatch = None
ShouldTerminate = false
Subscribers = ResizeArray()
PreviousStore = None
}

let dispatch msg =
match finalDispatch with
match runtime.FinalDispatch with
| Some finalDispatch -> finalDispatch msg
| None -> failwith "You're using initial dispatch. That shouldn't happen."

let subscribers = ResizeArray<unit -> unit>()

let subscribe callback =
subscribers.Add(callback)
fun () -> subscribers.Remove(callback) |> ignore
runtime.Subscribers.Add(callback)
fun () -> runtime.Subscribers.Remove(callback) |> ignore

let mapSetState setState model dispatch =
setState model dispatch
let oldModel = state
state <- Some model
finalDispatch <- Some dispatch
let oldModel = runtime.State
runtime.State <- Some model
runtime.FinalDispatch <- Some dispatch

match runtime.PreviousStore with
| Some previousStore ->
previousStore.SetTermination true
runtime.PreviousStore <- None
| None -> ()

// Skip re-renders if model hasn't changed
if not (obj.ReferenceEquals(model, oldModel)) then
subscribers |> Seq.iter (fun callback -> callback ())
runtime.Subscribers |> Seq.iter (fun callback -> callback ())

let mapInit userInit arg =
if state.IsSome then state.Value, Cmd.none else userInit arg
match runtime.PreviousStore with
| Some previousStore ->
let model = previousStore.Store.GetModel()
runtime.State <- Some model
model, Cmd.none
| None -> userInit arg

let mapTermination (predicate, terminate) =
let pred msg = predicate msg || shouldTerminate
let pred msg = predicate msg || runtime.ShouldTerminate
pred, terminate

program
|> Program.map mapInit id id mapSetState id mapTermination
|> Program.runWith arg

let getState () =
match state with
match runtime.State with
| Some state -> state
| None -> failwith "State is not initialized. That shouldn't happen."

Expand All @@ -76,64 +100,98 @@ module ElmishStore =
Subscribe = UseSyncExternalStoreSubscribe subscribe
}

let storeState = {
Store = store
SetTermination = setTermination
let preparedProgram =
program
|> Program.map mapInit id id mapSetState id mapTermination

let run (program: Program<'arg, 'model, 'msg, 'view>) (arg: 'arg) =
runtime.PreviousStore <- Internal.tryGetStoreState runtime.UniqueName
runtime.ShouldTerminate <- false

let setTermination should = runtime.ShouldTerminate <- should

program
|> Program.runWith arg

let storeState = {
Store = store
SetTermination = setTermination
}

Internal.stores[runtime.UniqueName] <- box storeState
store

{
Program = preparedProgram
Run = run
}

stores[uniqueName] <- box storeState
store
let mapProgram
(mapper: Program<'arg, 'model, 'msg, 'view> -> Program<'arg, 'model, 'msg, 'view>)
(elmishProgram: ElmishProgramState<'arg, 'model, 'msg, 'view>)
=
{
Program = mapper elmishProgram.Program
Run = fun program arg -> elmishProgram.Run program arg
}

let createStoreWith uniqueName (arg: 'arg) (program: Program<'arg, 'model, 'msg, unit>) =
let runWith (arg: 'arg) (elmishProgram: ElmishProgramState<'arg, 'model, 'msg, 'view>) =
elmishProgram.Run elmishProgram.Program arg

let getState =
if stores.ContainsKey(uniqueName) then
let storeState = stores[uniqueName] |> unbox<StoreState<'arg, 'model, 'msg>>
storeState.SetTermination true
(fun () -> Some(storeState.Store.GetModel()))
else
(fun () -> None)
let inline run (elmishProgram: ElmishProgramState<unit, 'model, 'msg, 'view>) =
runWith () elmishProgram

initiate uniqueName arg program getState
let createStoreWith (uniqueName: string) (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) =
program
|> setup uniqueName
|> runWith arg

let inline createStore uniqueName program : ElmishStore<'model, 'msg> =
let inline createStore (uniqueName: string) (program: Program<unit, 'model, 'msg, 'view>) =
createStoreWith uniqueName () program

module ElmishStore =

let createStoreWith (uniqueName: string) (arg: 'arg) (program: Program<'arg, 'model, 'msg, 'view>) =
ElmishProgram.createStoreWith uniqueName arg program

let inline createStore (uniqueName: string) (program: Program<unit, 'model, 'msg, 'view>) : ElmishStore<'model, 'msg> =
ElmishProgram.createStore uniqueName program

[<Erase>]
type ElmishStore =

static member Create
(
program: Program<'arg, 'model, 'msg, unit>,
arg: 'arg,
uniqueName
uniqueName: string
) : ElmishStore<'model, 'msg> =
ElmishStore.createStoreWith uniqueName arg program

static member inline Create(program: Program<unit, 'model, 'msg, unit>, uniqueName) =
static member inline Create(program: Program<unit, 'model, 'msg, unit>, uniqueName: string) =
ElmishStore.Create(program, (), uniqueName)

static member inline Create
(
init: 'arg -> 'model * Cmd<'msg>,
update: 'msg -> 'model -> 'model * Cmd<'msg>,
arg: 'arg,
uniqueName
uniqueName: string
) =
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
uniqueName: string
) =
ElmishStore.Create(Program.mkProgram init update (fun _ _ -> ()), uniqueName)

static member inline Create
(
init: 'model * Cmd<'msg>,
update: 'msg -> 'model -> 'model * Cmd<'msg>,
uniqueName
uniqueName: string
) =
ElmishStore.Create(Program.mkProgram (fun () -> init) update (fun _ _ -> ()), uniqueName)
Loading