feat: Component reactiveRendering prop preventing view and update from getting stale#1504
feat: Component reactiveRendering prop preventing view and update from getting stale#1504mtamc wants to merge 1 commit into
reactiveRendering prop preventing view and update from getting stale#1504Conversation
…ns from getting stale
This commit makes it so, by setting a new `reactiveRendering` boolean field on `Component`, you can now feed state-derived parameters to your components, and your component will always be rendered with the latest version of your parameters. The parameters can be derived from parent, or even from ancestors higher up the component tree.
The way you would do this is simply like this, where in the below snippet `childComponent` parameters would previously become stale on state change (a notorious footgun in 1.9):
```hs
viewModel :: Model -> View Model Action
viewModel model = "child" +> childComponent (modelToInt model) (modelToBool model)
childComponent :: Int -> Bool -> Component parent Child ChildAction
childComponent statefulInt statefulBool =
(component 0 (updateChild statefulBool) (viewChild statefulInt statefulBool))
{ reactiveRendering = True
}
```
(See associated PR for video demonstration. You can even depend on stateful data derived from ancestors anywhere up the tree!)
This is achieved with the following strategy:
[1] `scheduler` marks dirty not just the dirty component, but also any child with `reactiveRendering` set to `True`, recursing down to descendants.
[2] `_componentDraw` no longer draws using the `view` function enclosed on mount, instead grabbing the latest view function from a new `_componentView` prop in `ComponentState` (if component `reactiveRendering` is `True`)
[3] `_componentApplyAction` similarly, no longer uses the `update` function enclosed on mount, instead grabbing the latest update function from a new `_componentUpdate` prop in `ComponentState` (if component `reactiveRendering` is `True`)
[4] `_componentView` and `_componentUpdate` are kept updated whenever `buildVTree` is run. When `buildVTree` processes a parent `VComp`, it already has access to the latest `viewModel` and `updateModel` functions with the latest dependencies, however it currently only uses them if it ends up performing a full component mount. Now, even if not performing a full component mount, we update the `ComponentState` with the latest `view` and `update` functions.
I'm not sure if performing this action inside `buildVTree` like would cause any issue with the Miso architecture, need feedback.
CAVEATS/POTENTIAL FOR FUTURE IMPROVEMENT:
[1] This currently requires the child to have a component key and therefore does not work with `mount_`. Perhaps this could be improved not to require a component key by finding children by mount order or something, but it didn't seem worth the headache.
[2] As of this commit, there still are some niche scenarios where data you pass into your component function become stale. I wanted to keep this commit clean for now, but there are other fields that could be updated by `buildVTree` in order for them not to get stale when defined in terms of your component props: `styles`, `scripts`, `logLevel`, `mailbox`, `eventPropagation`, `unmount`.
The following fields do not make sense to make reactive in this way: `model`, `hydrateModel`, `mountPoint,` `bindings`, `mount`
Finally, `subs` could be risky to kill and re-create on every re-render. Might have to pass on making `subs` reactive in this same way.
I can work on the above props in a later commit if needed.
[3] `reactiveRendering` means a component re-renders any time its parent is re-rendered. This is the default behavior on React (and the only behavior in Elm and legacy Zoom-style TEA-Miso), and is fine performance-wise for the vast majority of applications. It makes sense as well: children are a part of their parents, not offshoot.
That said, React provides the `memo` function which cause re-renders when a component "props" have changed. This ostensibly is not the default behavior because equality checks can actually be more costly than re-renders. `memo` also provides a parameter to supply a `arePropsEqual` function which mirrors how we'd use a custom `Eq` instance in Haskell.
Although I don't believe this option is crucial to have, if we decided to add it to Miso components, then the very nice and natural pattern of `component_ a b c = component initModel (updateModel b) (viewModel a b c)` would not be compatible with it and become a footgun again, because component dependencies would have to be one single typed expression with an `Eq` instance, stored as a record field (say `props`). (I would love to be proven wrong with some variadic type trickery or something...)
|
Update regarding type BoundVal = MisoString -- example
childComponent :: (parent -> BoundVal) -> Component parent Child ChildAction
childComponent parentToBoundVal = (component 0 (const $ pure ()) (const H.div_ [] []))
{ bindings = [ParentToChild parentToBoundVal (lensChildToBoundField .~)]
}So I think this is pretty nice. For any scenario where we need to pass data down to descendants and don't need to worry about rerenders, we can use |
e53cd20 to
c13e1e8
Compare
|
related #1516 |
|
Closing in favor of 3c6a08d |
Summary
This commit makes it so, by setting a new
reactiveRenderingboolean field onComponent(defaultFalse), you can now feed state-derived parameters to your components, and your component will always be rendered with the latest version of your parameters. The parameters can be derived from parent, or even from ancestors higher up the component tree.The way you would do this is simply like this, where in the below snippet
childComponentparameters would previously become stale on state change (a notorious footgun in 1.9):Demo
The code for this demo can be found here: https://github.com/mtamc/miso-sampler/blob/experiment/reactive_component_props/app/Main.hs
Before /
reactiveRendering = Falsereactive_prop_fail-2026-04-25_20.29.26.webm
After
reactive_prop_success-2026-04-25_19.25.37.webm
Method explanation
This is achieved with the following strategy:
[1]
schedulermarks dirty not just the dirty component, but also any child withreactiveRenderingset toTrue, recursing down to descendants.[2]
_componentDrawno longer draws using theviewfunction enclosed on mount, instead grabbing the latest view function from a new_componentViewprop inComponentState(if componentreactiveRenderingisTrue)[3]
_componentApplyActionsimilarly, no longer uses theupdatefunction enclosed on mount, instead grabbing the latest update function from a new_componentUpdateprop inComponentState(if componentreactiveRenderingisTrue)[4]
_componentViewand_componentUpdateare kept updated wheneverbuildVTreeis run. WhenbuildVTreeprocesses a parentVComp, it already has access to the latestviewModelandupdateModelfunctions with the latest dependencies, however it currently only uses them if it ends up performing a full component mount. Now, even if not performing a full component mount, we update theComponentStatewith the latestviewandupdatefunctions.I'm not sure if performing this action inside
buildVTreelike would cause any issue with the Miso architecture, need feedback.Caveats / Potential for future improvement
[1] This currently requires the child to have a component key and therefore does not work with
mount_. Perhaps this could be improved not to require a component key by finding children by mount order or something, but it didn't seem worth the headache.[2] As of this commit, there still are some niche scenarios where data you pass into your component function become stale. I wanted to keep this commit clean for now, but there are other fields that could be updated by
buildVTreein order for them not to get stale when defined in terms of your component props:styles,scripts,logLevel,mailbox,eventPropagation,unmount.The following fields do not make sense to make reactive in this way:
model,hydrateModel,mountPoint,bindings,mountFinally,
subscould be risky to kill and re-create on every re-render. Might have to pass on makingsubsreactive in this same way.I can work on the above props in a later commit if needed.
[3]
reactiveRenderingmeans a component re-renders any time its parent is re-rendered. This is the default behavior on React (and the only behavior in Elm and legacy Zoom-style TEA-Miso), and is fine performance-wise for the vast majority of applications. It makes sense as well: children are a part of their parents, not offshoot.That said, React provides the
memofunction which causes re-renders to only happen when a component "props" have changed. This ostensibly is not the default behavior because equality checks can actually be more costly than re-renders.memoalso provides a parameter to supply aarePropsEqualfunction which mirrors how we'd use a customEqinstance in Haskell.Although I don't believe this option is crucial to have, if we decided to add it to Miso components, then the very nice and natural pattern ofcomponent_ a b c = component initModel (updateModel b) (viewModel a b c)would not be compatible with it and become a footgun again, because component dependencies would have to be one single typed expression with anEqinstance, stored as a record field (sayprops). (I would love to be proven wrong with some variadic type trickery or something...)edit: I believe we already have the option to have
memo-like rendering behavior usingMiso.Binding, see followup post #1504 (comment)