Skip to content

feat: Component reactiveRendering prop preventing view and update from getting stale#1504

Closed
mtamc wants to merge 1 commit into
dmjio:masterfrom
mtamc:feat/reactive_component_props
Closed

feat: Component reactiveRendering prop preventing view and update from getting stale#1504
mtamc wants to merge 1 commit into
dmjio:masterfrom
mtamc:feat/reactive_component_props

Conversation

@mtamc
Copy link
Copy Markdown

@mtamc mtamc commented Apr 25, 2026

Summary

This commit makes it so, by setting a new reactiveRendering boolean field on Component (default False), 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):

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
        }

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 = False

reactive_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] 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 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. 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...)

edit: I believe we already have the option to have memo-like rendering behavior using Miso.Binding, see followup post #1504 (comment)

…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...)
@mtamc
Copy link
Copy Markdown
Author

mtamc commented Apr 26, 2026

Update regarding memo. I think you can already get memo-like behavior by keeping reactiveRendering set to False, and storing your ancestor-derived props as a Binding instead. Now, the component will only re-render if the Bound values have changed (_componentModelDirty call inside propagateChildren, since Bound values are stored inside the model!). You are also free to declare a custom Eq instance for your Bound values.

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 reactiveRendering. If we want to only have rerenders when the passed data changes (like React.memo), we can use Miso.Binding. And we can even use a custom Eq instance if necessary.

@dmjio dmjio force-pushed the master branch 2 times, most recently from e53cd20 to c13e1e8 Compare May 14, 2026 12:54
@dmjio
Copy link
Copy Markdown
Owner

dmjio commented May 18, 2026

related #1516

Repository owner deleted a comment from raquelpn1979-tech May 18, 2026
Repository owner deleted a comment from raquelpn1979-tech May 18, 2026
@dmjio
Copy link
Copy Markdown
Owner

dmjio commented May 18, 2026

Closing in favor of 3c6a08d

@dmjio dmjio closed this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants