React / Sutil inspired state handling#179
Conversation
| type IConnectable = | ||
| inherit IDisposable | ||
| abstract InstanceId: Guid with get | ||
|
|
||
| type ITap<'signal> = | ||
| inherit IConnectable | ||
| abstract member CurrentSignal: 'signal with get | ||
| abstract member Subscribe : ('signal -> unit) -> IDisposable | ||
|
|
||
| type IWire<'signal> = | ||
| inherit ITap<'signal> | ||
| abstract member Send : 'signal -> unit |
There was a problem hiding this comment.
Not really happy with the naming here. Open for suggestions.
There was a problem hiding this comment.
I wonder if there would be value in utilizing the built-in IObservable<'T>, IObserver<'T> and Subject<'T> for the underlying pub/sub mechanism.
While I admittedly don't quite have my head wrapped around the nuances of everything Wire.fs, it seems like it is very similar.
Potential benefits might be:
- Mainly, the ability for an end user to take advantage of more advanced Rx capabilities on the incoming subscriptions.
For example: if you wanted to share anIObservable<string>of keystrokes from one component to another, the user could use Rxdebounceto limit observed messages to avoid unnecessary refreshes. This is a fairly common request that would be a pain to implement manually. - A secondary benefit would be a familiar pub/sub model with known names (
IObservableinstead ofITap,Subjectinstead ofIWire, etc).
There was a problem hiding this comment.
Initially considered this, but
IObservabledoes not contain a value.
I had to look it up, but IEvent already inherits from IObservable, so it seems that the door is still open to allowing integration with Rx.
| open Avalonia.Layout | ||
| open Avalonia.FuncUI | ||
|
|
||
| let contactListView (contacts: ITap<Contact list>, selectedId: IWire<Guid option>, filter: IWire<string option>) = |
|
This looks really cool! |
|
Looking at your ContactBook example now... |
Great to hear! There are a few things I'm not yet sure about:
let counter = React.useState 0
React.useEffectOnce (fun _ ->
counter.set (counter.current + 1)
counter.set (counter.current + 1)
counter.set (counter.current + 1)
)
counter.set (counter.current + 1)
counter.set (counter.current + 1)
counter.set (counter.current + 1)
// current render 'counter' = 0
// next render 'counter' = 1Not sure yet if we should copy this behaviour.
Alternatives to make a call to ctx.useWhatever unique could be:
@JordanMarr what do you think? |
I like publish/subscribe because it is used a lot in UI frameworks for sharing between view models in MVVM. Rx uses Observable/Observer/Subject (where Subject can publish and subscribe). Still not as clear as publish/subscriber though IMO.
I don’t have a strong opinion because I am wary of multiple renders in React and so I would never do this - I would just set one state at time. I also only use a single useState at a time to prevent confusing multi pass render scenarios.
I am already used to React not allowing conditional hooks, so this limitation doesn’t really bother me. |
|
One of the most exciting features I have seen in a while for F# on desktop 😍 looking forward to dig into this further. One thing I didn't see in the example is the array dependencies for effects which re-triggers the effect when one of it's dependencies change: // currently this is the API usage
let contact = ctx.useTap contact
let image = ctx.useAsync Api.randomImage
// In case the image is _dependant_ on the contract
let contact = ctx.useTap contact
let image = ctx.useAsync(Api.contactImage contact.Id, [ contact.Id ]) |
JordanMarr
left a comment
There was a problem hiding this comment.
The new names are much more representative of their intended functionality in this context. Very nice!
| ) | ||
|
|
||
| let mainView () = | ||
| Component (fun ctx -> |
There was a problem hiding this comment.
When should one use the Component ctor vs Component.create?
There was a problem hiding this comment.
The constructor returns a Component (inherits ContentControl) while the create function returns a View<Component> that can be used in the DSL context.
So one if for the DSL context and one if for Avalonia context.
| type IConnectable = | ||
| inherit IDisposable | ||
| abstract InstanceId: Guid with get | ||
|
|
||
| type ITap<'signal> = | ||
| inherit IConnectable | ||
| abstract member CurrentSignal: 'signal with get | ||
| abstract member Subscribe : ('signal -> unit) -> IDisposable | ||
|
|
||
| type IWire<'signal> = | ||
| inherit ITap<'signal> | ||
| abstract member Send : 'signal -> unit |
There was a problem hiding this comment.
I wonder if there would be value in utilizing the built-in IObservable<'T>, IObserver<'T> and Subject<'T> for the underlying pub/sub mechanism.
While I admittedly don't quite have my head wrapped around the nuances of everything Wire.fs, it seems like it is very similar.
Potential benefits might be:
- Mainly, the ability for an end user to take advantage of more advanced Rx capabilities on the incoming subscriptions.
For example: if you wanted to share anIObservable<string>of keystrokes from one component to another, the user could use Rxdebounceto limit observed messages to avoid unnecessary refreshes. This is a fairly common request that would be a pain to implement manually. - A secondary benefit would be a familiar pub/sub model with known names (
IObservableinstead ofITap,Subjectinstead ofIWire, etc).
Initially considered this, but
https://fsprojects.github.io/FSharp.Data.Adaptive/ could work instead of our own values, but I don't fully get it. From looking at the code it does a lot I don't understand (yet). |
@Zaid-Ajaj Thanks for the kind words. I have time next week to hopefully make some progress on this. If you have any suggestions just let me know. Happy to discuss anything. |
|
I haven't had a chance to properly check all of the code, but I gotta say that this looks absolutely amazing and really promising! 😃 Just a couple of thoughts I have in my head after a quick check:
|
Still an early prototype. Effects are tricky, me might need to change a lot to support them properly. Wet paint everywhere 😁
Like your suggestions, would favor names that are different from react. I fear (and I might be wrong) that having similar names with different behavior could confuse people. Think we should just use the names you suggested for now tho. Might be good to also rename 'Value' to 'State' then 🤔
I have an implementation that adds effects locally. It currently is super buggy, will post here in a few days with more details/ the problems that come with effects. We should not run effects during render or directly when a dependency changes I think. So we need to have something like an 'Executor' that either runs effects from some kind of queue or renders the component. But never both at the same time.
The 'useAsync' hook should be based on a state + effect hook I think - so that we can support reloading/invalidation. |
UpdateBetter NamesComponent (fun ctx ->
let contacts = ctx.usePassedState ContactStore.shared.Contacts
let selectedId = ctx.useState (None: Guid option)
let filter = ctx.useState (None: string option)
..
)Conditionally render hooks.This is archived by using the line number a hook was invoked from as the identity instead of the call order. The line number is implicitly passed. type Context with
member this.useState<'value>(init: 'value, [<CallerLineNumber>] ?identity: int) : IWritable<'value> =
this.useHook<'value>(
StateHook.Create (
identity = $"Line: {identity.Value}",
state = StateHookValue.Lazy (fun _ -> new State<'value>(init) :> IConnectable),
renderOnChange = true
)
) :?> IWritable<'value>
..EffectsRender only once after component is initialised / after first render Component (fun ctx ->
this.useEffect (
handler = (fun _ ->
..
),
triggers = [ EffectTrigger.AfterInit ]
)
..
)Render after every render. Component (fun ctx ->
this.useEffect (
handler = (fun _ ->
..
),
triggers = [ EffectTrigger.AfterRender ]
)
..
)Render when dependencies changed. Component (fun ctx ->
let contacts = ctx.usePassedState ContactStore.shared.Contacts
let selectedId = ctx.useState (None: Guid option)
this.useEffect (
handler = (fun _ ->
..
),
triggers = [
EffectTrigger.AfterChange selectedId.Any
EffectTrigger.AfterChange selectedContact.Any
]
)
..
)@Zaid-Ajaj @JordanMarr @sleepyfran Thoughts? |
My thoughts are that you are on 🔥! I like the React-y hook names. One question about re-rendering when dependencies change: triggers = [
EffectTrigger.AfterChange selectedId.Any
EffectTrigger.AfterChange selectedContact.Any
]Is it possible to pass in any value on the model to AfterChange? |
Types that implement Avalonia.FuncUI/src/Avalonia.FuncUI.Components/State/State.fs Lines 46 to 59 in 7d08933
Right now it does not even check if there was really a change to the value. It just subscribes to the passed dependencies. Those dependencies might have a unique value filter. It's also possible to "focus" state and add a unique value filter to it. Component.create ("Person", fun ctx ->
let name =
person
|> State.readMap (fun person -> person.FullName)
|> State.readUnique
|> ctx.usePassedState (* the context already supports state that does not trigger a render on change. there are just no overloads for it yet. *)
this.useEffect (
handler = (fun _ ->
..
),
triggers = [
EffectTrigger.AfterChange name.Any
]
)
..
) |
edcb9f4 to
5c2743f
Compare
|
To test more advanced scenarios I ported an app (FSharpGeneticAlgorithm) from @IntegerMan to FuncUI and the new state management. There are still a few small bugs I need to fix, but it went pretty smooth overall. |
|
@JaggerJo This looks absolutely amazing 😍 😍 can't wait to try it out! |
|
Nice! |
Could be a nice app to include in the Examples if that would be ok for you. |
I personally never liked the way react does this and would not include a function that seperates current and set right now. It can easily be added tho by people not liking the current model - but if you ever need to pass that state down you need to reconstruct a |
ChordParser example with useElmish hook
|
Super busy right now unfortunately. This is ready to merge after combining the |
Awesome to hear it's ready! I'll take care of combining both modules, solving all the conflicts and merging tomorrow 😃 |
IIRC there is already a |
|
@sleepyfran let me know if you need help! |
Thanks :) I actually haven't had that much time, but I started the process the other day on the branch universal-hooks-moving, having some compilation issues that for some reason didn't arise when the code was in a separate project, but as I said I haven't had much time to look into it, hopefully I can finish the work soon. How should we name the code that is currently sitting in the components folder, though? It's just the Hosts, Lazy and DataTemplateView, so not sure what the correct naming for that one should be. |
No worries, can relate. Time is flying by right now. Regarding the stuff currently in the components folder:
|
Thanks a lot! I finished the whole thing and published the PR in #188, take a look whenever you get some time, but this should be ready to roll! 😃 |
# Conflicts: # src/Avalonia.FuncUI/Avalonia.FuncUI.fsproj # src/Examples/Examples.CounterApp/Examples.CounterApp.fsproj
|
It looks like there were still some conflicts even after merging my branch, should be good now. Also bumped the dotnet version in the Actions so that they don't fail while trying to compile the Chord Parser project 😃 |
|
We're live 🚀 Thanks @JaggerJo for the amazing work 😃 |
|
@sleepyfran nice! @JordanMarr How do we go about releasing this? I guess we should also change the package name to not include 'Jaggerjo'. |
1 similar comment
|
@sleepyfran nice! @JordanMarr How do we go about releasing this? I guess we should also change the package name to not include 'Jaggerjo'. |
Agree, we were having that conversation as part of #173. My favorite option was |
|
Also created #189 to track the steps needed for before/after the release of the new version so that the docs and other resources are in good health, feel free to suggest any other steps to be added 😃 |

This is a prototype for a React/Sutil inspired state handling library for FuncUI.
There is an example contact app included to test it.
Feedback is appreciated.