Conversation
This adds the `VProp` constructor to the miso `View` DSL. The goal of this PR is to emulate the React "props" feature. - https://react.dev/learn/passing-props-to-a-component Always redraw if `useProps` is `True`. This must be opted into currently. - [x] Parameterize `View` by `parent` - [x] Add `VProp` constructor case - [x] Add `useProps`, `_componentUseProps`
mtamc
left a comment
There was a problem hiding this comment.
Issue: Component using VProp does not seem to get re-rendered with latest values. It seems stuck on its initial value. I haven't dug into the code to figure out why. See my minimal usage attempt: https://github.com/mtamc/miso-sampler/blob/experiment/vprops/app/Main.hs#L76
Thought: I'm still experimenting with the new props, I'll make a follow-up post when I'm done figuring out all I can do with it.
|
After trying this out, this API in its current form would not be appealing for me to use over a ParentToChild binding at the moment. I'll explain why with an example of React component props passing: counters-2026-04-24_15.39.53.webmThe renders an array of counters, and each counter has a "Foo" stateful child whose color depends on the parent counter value, and the position of the child's associated counter (the first counter's color is fixed to blue). import { useState } from 'react';
export default function App () {
const [counters, setCounters] = useState([0, 0, 0, 0]);
return (
<div>
<h1>🍜 Miso counters</h1>
<div>
Counter total: {counters.reduce((a, b) => a + b, 0)}
</div>
{counters.map((counter, i) => (
<Counter
key={"counter-" + i}
i={i}
counter={counter}
onChange={(delta) => {
setCounters((old) => old.map((c, j) => (j === i ? c + delta : c)));
}}
/>
))}
</div>
);
};
const Counter = ({ i, counter, onChange }) => (
<div>
<button onClick={() => onChange(1)}>+</button>
<button onClick={() => onChange(-1)}>-</button>
{" | "}
{counter}
{" | "}
<Foo
attrs={
i === 0 ? {style: {color: "blue"}}
: counter < 0 ? {style: {color: "red"}}
: counter > 0 ? {style: {color: "green"}}
: {}
}
/>
</div>
);
const Foo = ({ attrs }) => {
const [timesClicked, setTimesClicked] = useState(0);
return (
<span {...attrs} onClick={() => setTimesClicked((old) => old + 1)}>
{" | (Foo here! Foo was clicked "}
{timesClicked}
{" times)"}
{" | "}
</span>
);
};You can try it online: https://codesandbox.io/p/devbox/dry-breeze-t37n6f Notice how we are declaring This is how I was able to replicate that with data AppModel = AppModel {_counters :: [Int]} deriving (Eq, Show)
counters = lens (\(AppModel cs) -> cs) (\(AppModel _) cs -> AppModel cs)
data Action = OnChange Int Int deriving (Show, Eq)
app :: App AppModel Action
app = (component (AppModel [0, 0, 0, 0]) updateModel viewModel)
updateModel :: Action -> Effect parent AppModel Action
updateModel (OnChange i delta) = compose (at i) counters %= fmap (+ delta)
viewModel :: AppModel -> View parent AppModel Action
viewModel x =
H.div_ [] $
H.h1_ [] ["🍜 Miso counters"]
: H.div_
[]
[ "Counter total: "
, text . ms . show . sum $ x ^. counters
]
: zipWith viewCounter [0 ..] (x ^. counters)
viewCounter :: Int -> Int -> View parent AppModel Action
viewCounter i counter =
H.div_
[]
[ H.button_ [E.onClick $ OnChange i 1] ["+"]
, H.button_ [E.onClick $ OnChange i (-1)] ["-"]
, " | "
, text $ ms counter
, ("foo-" <> ms i) +> fooComponent i
]
instance HasField "fooAttrs" AppModel (Int -> [Attribute FooAction]) where
getField mdl i
| i == 0 = [CSS.style_ [CSS.color CSS.blue]]
| (mdl ^. counters) !! i < 0 = [CSS.style_ [CSS.color CSS.red]]
| (mdl ^. counters) !! i > 0 = [CSS.style_ [CSS.color CSS.black]]
| otherwise = []
-- Child component Foo
data Foo = Foo {_timesClicked :: Int} deriving (Show, Eq)
timesClicked = lens (\(Foo tc) -> tc) (\(Foo _) tc -> Foo tc)
data FooAction = FooClicked deriving (Show, Eq)
fooComponent ::
(HasField "fooAttrs" parent (Int -> [Attribute FooAction])) =>
Int -> Component parent Foo FooAction
fooComponent i = (component (Foo 0) (pure $ timesClicked += 1) (viewFoo i))
{ useProps = True }
viewFoo ::
(HasField "fooAttrs" parent (Int -> [Attribute FooAction])) =>
Int -> Foo -> View parent Foo FooAction
viewFoo i mdl = props @"fooAttrs" $ \iToFooAttrs ->
H.span_
(iToFooAttrs i)
[ " | (Foo here! Foo was clicked "
, text $ ms $ show (mdl ^. timesClicked)
, " times)"
](Another test case I wanted to try was make Here's how I would achieve the same with reactive bindings and #1501, the function signatures are much cleaner, and the logic to determine the attrs is next to where we mount the child component, similar to React: viewCounter :: Int -> Int -> View parent AppModel Action
viewCounter i counter =
H.div_
[]
[ H.button_ [E.onClick $ OnChange i 1] ["+"]
, H.button_ [E.onClick $ OnChange i (-1)] ["-"]
, " | "
, text $ ms counter
, ("foo-" <> ms i)
+> fooComponent
( \mdl ->
if
| i == 0 -> [CSS.style_ [CSS.color CSS.blue]]
| (mdl ^. counters) !! i < 0 -> [CSS.style_ [CSS.color CSS.red]]
| (mdl ^. counters) !! i > 0 -> [CSS.style_ [CSS.color CSS.black]]
| otherwise -> []
)
]
-- Child component Foo
data Foo = Foo
{ _timesClicked :: Int
, _attrs :: [Attribute FooAction]
}
deriving (Show, Eq)
timesClicked = lens (\(Foo tc _) -> tc) (\foo tc -> foo{_timesClicked = tc})
attrs = lens (\(Foo _ as) -> as) (\foo as -> foo{_attrs = as})
data FooAction = FooClicked deriving (Show, Eq)
fooComponent ::
(parent -> [Attribute FooAction]) ->
Component parent Foo FooAction
fooComponent parentToAttrs =
(component (Foo 0 []) (pure $ timesClicked += 1) viewFoo)
{ bindings = [ParentToChild parentToAttrs (attrs .~)]
}
viewFoo :: Foo -> View parent Foo FooAction
viewFoo mdl =
H.span_
(mdl ^. attrs)
[ " | (Foo here! Foo was clicked "
, text . ms . show $ mdl ^. timesClicked
, " times)"
]This is already quite nice, but here's what this would look like in the dream scenario: viewCounter :: Int -> Int -> View parent AppModel Action
viewCounter i counter =
H.div_
[]
[ H.button_ [E.onClick $ OnChange i 1] ["+"]
, H.button_ [E.onClick $ OnChange i (-1)] ["-"]
, " | "
, text $ ms counter
, ("foo-" <> ms i)
+> fooComponent
(if | i == 0 -> [CSS.style_ [CSS.color CSS.blue]]
| counter < 0 -> [CSS.style_ [CSS.color CSS.red]]
| counter > 0 -> [CSS.style_ [CSS.color CSS.black]]
| otherwise -> []
)
]
-- Child component Foo
data Foo = Foo { _timesClicked :: Int } deriving (Show, Eq)
timesClicked = lens (\(Foo tc) -> tc) (\foo tc -> foo{_timesClicked = tc})
data FooAction = FooClicked deriving (Show, Eq)
fooComponent :: [Attribute FooAction] -> Component parent Foo FooAction
fooComponent attrs = component (Foo 0) (pure $ timesClicked += 1) (viewFoo attrs)
viewFoo :: [Attribute FooAction] -> Foo -> View parent Foo FooAction
viewFoo attrs mdl =
H.span_
attrs
[ " | (Foo here! Foo was clicked "
, text . ms . show $ mdl ^. timesClicked
, " times)"
] |
This adds the
VPropconstructor to the misoViewDSL. The goal of this PR is to emulate the React "props" feature.Always redraw if
usePropsisTrue. This must be opted into currently.ViewbyparentVPropconstructor caseuseProps,_componentUsePropsVPropscase for renderingHTML