-
Notifications
You must be signed in to change notification settings - Fork 93
Fix: Feliz.Elmish SSG #697
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
44b81d2
a707718
3235c49
5f61c85
564bc0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ sidebar_position: 9 | |
|
|
||
| import ComponentRender from '@site/src/components/ComponentRender'; | ||
| import CodeBlock from '@theme/CodeBlock'; | ||
| import BrowserOnly from '@docusaurus/BrowserOnly'; | ||
|
|
||
| Besides being able to use Feliz in existing Elmish applications, you can also use Elmish as _part_ of your Feliz application. This is a different approach to building standalone React components that use Elmish internally to manage the state of the component but from the perspective of the consumer, it is just another React component. | ||
|
|
||
|
|
@@ -30,11 +31,18 @@ Unable to show live example because Feliz.UseElmish does not support Server-Side | |
|
|
||
| Here is an example to demonstrate how to build such component: | ||
|
|
||
| import ElmishCounter from '../../feliz-docs/fableoutput/Examples/React/ElmishCounter' | ||
| import RawElmishCounter from '!!raw-loader!../../feliz-docs/Examples/React/ElmishCounter.fs' | ||
|
|
||
| <CodeBlock language="fsharp" showLineNumbers> | ||
| {RawElmishCounter} | ||
| </CodeBlock> | ||
| <ComponentRender code={RawElmishCounter}><ElmishCounter /></ComponentRender> | ||
|
||
|
|
||
|
|
||
| <BrowserOnly fallback={<div>Loading in browser...</div>} > | ||
| {() => | ||
| <ComponentRender code={RawElmishCounter}><ElmishCounter /></ComponentRender> | ||
| } | ||
| </BrowserOnly> | ||
|
|
||
|
|
||
| The difference here from a full-fledged Elmish applications is that there isn't an "Elmish entry point" to run the component and manage its life-cycle. Instead, the `React.useElmish` hooks manages the Elmish life-cycle internally within the React component so that it can run standalone inside other React components: | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,19 +1,10 @@ | ||||||||||||||||||
| module Feliz.UseElmish | ||||||||||||||||||
|
|
||||||||||||||||||
| open System | ||||||||||||||||||
| open Fable.Core | ||||||||||||||||||
| open Elmish | ||||||||||||||||||
|
|
||||||||||||||||||
| module private Util = | ||||||||||||||||||
| type UseSyncExternalStoreSubscribe = delegate of (unit -> unit) -> (unit -> unit) | ||||||||||||||||||
|
|
||||||||||||||||||
| [<ImportMember("react")>] | ||||||||||||||||||
| let useSyncExternalStore(subscribe: UseSyncExternalStoreSubscribe, getSnapshot: unit -> 'Model, getServerSnapshot: (unit -> 'Model) option): 'Model = jsNative | ||||||||||||||||||
|
|
||||||||||||||||||
| [<ImportMember("react")>] | ||||||||||||||||||
| let useState(init: unit -> 'State): 'State * ('State -> unit) = jsNative | ||||||||||||||||||
|
|
||||||||||||||||||
| [<ImportMember "react">] | ||||||||||||||||||
| let useEffect(effect: unit -> unit, dependencies: obj array) : unit = jsNative | ||||||||||||||||||
|
|
||||||||||||||||||
| [<Emit "setTimeout($0)">] | ||||||||||||||||||
| let setTimeout(callback: unit -> unit) : unit = jsNative | ||||||||||||||||||
|
|
@@ -47,7 +38,7 @@ module private Util = | |||||||||||||||||
| let subscribed = false | ||||||||||||||||||
| (model, initialDispatch, subscribed, queuedMessages), cmd | ||||||||||||||||||
|
|
||||||||||||||||||
| let subscribe = UseSyncExternalStoreSubscribe(fun callback -> | ||||||||||||||||||
| let subscribe = FsReact.createSyncExternalStoreSubscribe(fun callback -> | ||||||||||||||||||
| // printfn "Subscribing %O..." guid | ||||||||||||||||||
| let mutable dispose = false | ||||||||||||||||||
| // needsDispose is used to determine whether the model inside state needs to be disposed of when the subscription is terminated. | ||||||||||||||||||
|
|
@@ -105,21 +96,22 @@ module private Util = | |||||||||||||||||
| member _.IsOutdated(arg', dependencies') = arg <> arg' || dependencies <> dependencies' | ||||||||||||||||||
|
|
||||||||||||||||||
| open Util | ||||||||||||||||||
| open Feliz | ||||||||||||||||||
|
|
||||||||||||||||||
| [<Erase>] | ||||||||||||||||||
| type React = | ||||||||||||||||||
| static member useElmish(program: unit -> Program<'Arg, 'Model, 'Msg, unit>, arg: 'Arg, ?dependencies: obj array): 'Model * ('Msg -> unit) = | ||||||||||||||||||
| let state, setState = useState(fun () -> ElmishState(program, arg, dependencies)) | ||||||||||||||||||
| let state, setState = React.useState(fun () -> ElmishState(program, arg, dependencies)) | ||||||||||||||||||
| if state.IsOutdated(arg, dependencies) then | ||||||||||||||||||
| ElmishState(program, arg, dependencies) |> setState | ||||||||||||||||||
| let finalState, dispatch, subscribed, queuedMessages = useSyncExternalStore(state.Subscribe, (fun () -> state.State), None) | ||||||||||||||||||
| let finalState, dispatch, subscribed, queuedMessages = React.useSyncExternalStore(state.Subscribe, UseSyncExternalStoreSnapshot(fun () -> state.State), UseSyncExternalStoreSnapshot(fun () -> state.State)) | ||||||||||||||||||
|
||||||||||||||||||
| let finalState, dispatch, subscribed, queuedMessages = React.useSyncExternalStore(state.Subscribe, UseSyncExternalStoreSnapshot(fun () -> state.State), UseSyncExternalStoreSnapshot(fun () -> state.State)) | |
| let initialState = state.State | |
| let finalState, dispatch, subscribed, queuedMessages = | |
| React.useSyncExternalStore( | |
| state.Subscribe, | |
| UseSyncExternalStoreSnapshot(fun () -> state.State), | |
| UseSyncExternalStoreSnapshot(fun () -> initialState) | |
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my tests and understanding this should be fine as the initial value should always be the same that is passed in from the init function.
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,3 +59,6 @@ type FsReact = | |||||||||||
| ) | ||||||||||||
| ) | ||||||||||||
| token | ||||||||||||
|
|
||||||||||||
| static member inline createSyncExternalStoreSubscribe(subscribe: (unit -> unit) -> (unit -> unit)) : UseSyncExternalStoreSubscribe = | ||||||||||||
| UseSyncExternalStoreSubscribe(subscribe) | ||||||||||||
|
||||||||||||
| UseSyncExternalStoreSubscribe(subscribe) | |
| UseSyncExternalStoreSubscribe(subscribe) | |
| static member inline createSyncExternalStoreSnapshot<'Snapshot>(getSnapshot: unit -> 'Snapshot) : UseSyncExternalStoreSnapshot<'Snapshot> = | |
| UseSyncExternalStoreSnapshot(getSnapshot) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed the helper as it seemed unnecessary and more verbose
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -636,25 +636,25 @@ useLayoutEffect(() => { | |
| /// <param name='getSnapshot'>A function that returns the current value of the external data source.</param> | ||
| /// <returns>The current value from the external data source.</returns> | ||
| [<ImportMember("react")>] | ||
| static member inline useSyncExternalStore(subscribe: Func<(unit -> unit),(unit -> unit)> , getSnapshot: unit -> 'T, ?getServerSnapshot: unit -> 'T): 'T = jsNative | ||
| static member inline useSyncExternalStore(subscribe: UseSyncExternalStoreSubscribe, getSnapshot: UseSyncExternalStoreSnapshot<'T>, ?getServerSnapshot: UseSyncExternalStoreSnapshot<'T>): 'T = jsNative | ||
|
|
||
| /// <summary> | ||
| /// Subscribes to a data source, and returns the current value from it. | ||
| /// </summary> | ||
| /// <param name='subscribe'>A function that sets up a subscription to the external data source. It receives a callback to be called when the data source changes.</param> | ||
| /// <param name='getSnapshot'>A function that returns the current value of the external data source.</param> | ||
| /// <returns>The current value from the external data source.</returns> | ||
| static member inline useSyncExternalStore(subscribe: (unit -> unit) -> (unit -> unit), getSnapshot: unit -> 'T, ?getServerSnapshot: unit -> 'T): 'T = | ||
| React.useSyncExternalStore( Func<_,_> subscribe, getSnapshot, ?getServerSnapshot = getServerSnapshot) | ||
| static member inline useSyncExternalStore(subscribe: (unit -> unit) -> (unit -> unit), getSnapshot: UseSyncExternalStoreSnapshot<'T>, ?getServerSnapshot: UseSyncExternalStoreSnapshot<'T>): 'T = | ||
| React.useSyncExternalStore( UseSyncExternalStoreSubscribe subscribe, getSnapshot, ?getServerSnapshot = getServerSnapshot) | ||
|
Comment on lines
+647
to
+648
|
||
|
|
||
| /// <summary> | ||
| /// Subscribes to a data source, and returns the current value from it. | ||
| /// </summary> | ||
| /// <param name='subscribe'>A function that sets up a subscription to the external data source. It receives a callback to be called when the data source changes.</param> | ||
| /// <param name='getSnapshot'>A function that returns the current value of the external data source.</param> | ||
| /// <returns>The current value from the external data source.</returns> | ||
| static member inline useSyncExternalStore(subscribe: (unit -> unit) -> #IDisposable, getSnapshot: unit -> 'T, ?getServerSnapshot: unit -> 'T): 'T = | ||
| React.useSyncExternalStore( Func<_,_> | ||
| static member inline useSyncExternalStore(subscribe: (unit -> unit) -> #IDisposable, getSnapshot: UseSyncExternalStoreSnapshot<'T>, ?getServerSnapshot: UseSyncExternalStoreSnapshot<'T>): 'T = | ||
| React.useSyncExternalStore( UseSyncExternalStoreSubscribe | ||
| (fun (callback) -> | ||
| let disp = subscribe(callback) | ||
| fun () -> disp.Dispose() | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,7 +35,6 @@ type Components = | |
| snapshot | ||
| ) | ||
|
|
||
|
|
||
| Html.div [ | ||
|
|
||
| ] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if you correctly handle the
getServerSnapshotvariable it should work without BrowserOnly component https://react.dev/reference/react/useSyncExternalStoreThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this was from an earlier test to just show the docs before I attempted making useElmish for SSG. Updated docs to remove duplicate and import.