Bolero
Bolero copied to clipboard
[Proposal] Reactive Components
Overview
Add "functional"/"reactive" components similar to React using something like Hooks for state management as an alternative to using Elmish.
Problem
Elmish is a very system for state management. However it comes with some costs, namely a large amount of boilerplate and significant complexity for deeply nested apps. The later is potentially solved by Nested Program Components (#236), however it looks like that feature never got implemented.
Proposed Solution
Defining components:
Users could define components simply as a function of shape IReactiveComponentContext -> Node
ie:
let counter ctx =
let count, setCount = ctx.useState(0).WithSetter()
ctx.useEffect(fun _ -> printfn "Counter instantiated!")
div {
text $"Count: {count}"
button {
on.click (fun _ -> setCount (count + 1))
"Increment"
}
}
Instantiating Components:
Reactive components would then be instantiated by a new rcomp
function (similar to comp
/ecomp
).
Hooks:
-
useState<'T>(initialValue: 'T): ReactiveValue<'T>
: Essentially the same as the useState hook from React. TheReactiveValue<'T>
type contains the current value for that state object as well as aSet
function to update state and trigger a re-render. AdditionallyReactiveValue
is anIObservable
so arbitrary change listeners can be attached. AWithSetter()
helper extension can be used to provide a similar experience to React (at the loss of theIObservable
object). -
useEffect(effect: unit -> unit, ?triggers: EffectTrigger list)
: Again very similar to useEffect from React. Essentially it allows users to provide a callback to be called under certain conditions. Valid triggers areAfterInit
,AfterRender
, andAfterChange of IObservable
Proof-of-Concept:
I have a proof-of-concept created at https://github.com/scottbot95/Bolero/tree/reactive. Happy to submit a PR if that will aid discussion.
Internally it works by creating a new ReactiveComponent
class that inherits Component
that manages state through a ReactiveComponentContext
object and will call the provided render function with the context object on each Render()
call.