Skip to content

Commit eb9fa23

Browse files
committed
Initial commit of typesafe React props. (#1518)
* Initial commit of typesafe `React` props. [Props](https://react.dev/learn/passing-props-to-a-component) are used in React to provide parent-to-child communication. This PR extends the `Component` and `View` classes, parameterizing them by both `props` and `parentAction`. `parentAction` allows a child `Component` to write to the `parent` `Sink`. This is useful when using the `props` feature to make use of the parent's `update` function from the `child` `Component`. This is a common pattern in React. The `withParentSink` function has been introduced to facilitate this. - [x] Adds `mountProps_`, `mountProps` combinators for storing 'props' on a 'VComp'. - [x] `Component`, `ComponentState` are now parameterized by `props`. - [x] `view` has been extended to receive a `Maybe props` argument. - [x] `parentAction` now parameterizes both `Component` and `Effect`. N.B. if `props` are `Nothing`, the diffing function will not diff props. The callback will not be invoked. Practically, this change ensures that "props" are propagated throughout a `Component` subtree, to all descendants, invoking the render phase only (bypassing the commit phase). * Drop `withParent` and `parentAction`. Adjust comment on `mountProps_` * `Maybe props` -> `props`, docs. Use `globalProps`. Pass in `initialProps` as `()`. * Clean up commented code in Effect.hs Removed commented-out code for someAction in Effect.hs. * Drop unneeded `liftIO`, guard `_componentDraw`. * Batch negated `ComponentId` during `dequeue`. * Refactor comments in Effect.hs for Lens functions Updated comments for clarity and consistency in Lens definitions. * `mountProps` -> `mountWithProps` * Replace `globalProps` w/ `_componentProps`
1 parent 184eaf7 commit eb9fa23

18 files changed

Lines changed: 490 additions & 216 deletions

File tree

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ app :: App Int Action
200200
app = vcomp 0 updateModel viewModel
201201
----------------------------------------------------------------------------
202202
-- | Updates model, optionally introduces side effects
203-
updateModel :: Action -> Effect parent Int Action
203+
updateModel :: Action -> Effect parent props Int Action
204204
updateModel = \case
205205
AddOne -> this += 1
206206
SubtractOne -> this -= 1
@@ -209,8 +209,8 @@ updateModel = \case
209209
consoleLog "Hello World"
210210
----------------------------------------------------------------------------
211211
-- | Constructs a virtual DOM from a model
212-
viewModel :: Int -> View Int Action
213-
viewModel x = vfrag
212+
viewModel :: () -> Int -> View Int Action
213+
viewModel _props x = vfrag
214214
[ H.button_ [ H.onClick AddOne ] [ text "+" ]
215215
, text (ms x)
216216
, H.button_ [ H.onClick SubtractOne ] [ text "-" ]

js/miso.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,8 @@ function diff(c, n, parent, context) {
282282
n.componentId = c.componentId;
283283
if (c.child)
284284
c.child.parent = n;
285+
if (n.diffProps)
286+
n.diffProps();
285287
return;
286288
}
287289
replace(c, n, parent, context);

js/miso.prod.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

miso.cabal

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
cabal-version: 2.2
22
name: miso
3-
version: 1.10.0.0
3+
version: 1.11.0.0
44
category: Web, Miso, Data Structures
55
license: BSD-3-Clause
66
license-file: LICENSE

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "haskell-miso",
3-
"version": "0.0.10",
3+
"version": "0.0.11",
44
"description": "miso: A tasty Haskell web and mobile framework",
55
"scripts": {
66
"clean": "tsc --build --clean && find ts -name '*~' -or -name '*.js' -delete && rm -rf out-tsc",

sample-app/Main.hs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Miso
99
import qualified Miso.Html as H
1010
import qualified Miso.Html.Property as P
1111
import Miso.Lens
12-
import Miso.Reload
12+
import Miso.Reload
1313
----------------------------------------------------------------------------
1414
-- | Component model state
1515
data Model
@@ -51,15 +51,15 @@ emptyModel :: Model
5151
emptyModel = Model 0
5252
----------------------------------------------------------------------------
5353
-- | Updates model, optionally introduces side effects
54-
updateModel :: Action -> Effect parent Model Action
54+
updateModel :: Action -> Effect parent props Model Action
5555
updateModel = \case
5656
AddOne -> counter += 1
5757
SubtractOne -> counter -= 1
5858
SayHelloWorld -> io_ (consoleLog "Hello world")
5959
----------------------------------------------------------------------------
6060
-- | Constructs a virtual DOM from a model
61-
viewModel :: Model -> View Model Action
62-
viewModel x =
61+
viewModel :: props -> Model -> View Model Action
62+
viewModel _ x =
6363
vfrag
6464
[ H.button_ [ H.onClick AddOne ] [ text "+" ]
6565
, text $ ms (x ^. counter)

src/Miso.hs

Lines changed: 162 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
-- This is the templating function that is used to construct a new virtual DOM
7474
-- (or HTML if rendering on the server).
7575
--
76-
-- * __update__: @'update' :: action -> 'Effect' parent model action@
76+
-- * __update__: @'update' :: action -> 'Effect' parent props model action@
7777
-- The 'update' function handles how the 'model' evolves over time in response
7878
-- to events that are raised by the application. This function takes any @action@,
7979
-- updating the @model@ and optionally introduces 'IO' into the system.
@@ -97,8 +97,8 @@
9797
--
9898
-- @
9999
-- data 'SomeComponent' parent
100-
-- = forall model action . Eq model
101-
-- => 'SomeComponent' ('Component' parent model action)
100+
-- = forall model action props . (Eq model, Eq props)
101+
-- => 'SomeComponent' props ('Component' parent model action)
102102
-- @
103103
--
104104
-- The smart constructors:
@@ -127,22 +127,23 @@
127127
-- import qualified Miso.Html.Property as HP
128128
-- -----------------------------------------------------------------------------
129129
-- * - The type of the parent Component 'model'
130-
-- | * - The type of the current Component's 'model'
131-
-- | | * - The type of the action that updates the 'model'
132-
-- | | |
133-
-- counter :: 'Component' parent Int Action
130+
-- | * - The type of the parent Component 'props' accessible to the child
131+
-- | | * - The type of the current Component's 'model'
132+
-- | | | * - The type of the action that updates the 'model'
133+
-- | | | |
134+
-- counter :: 'Component' ROOT () Int Action
134135
-- counter = 'vcomp' m u v
135136
-- where
136137
-- m :: Int
137138
-- m = 0
138139
--
139-
-- u :: Action -> 'Effect' parent Int Action
140+
-- u :: Action -> 'Effect' ROOT () Int Action
140141
-- u = \\case
141142
-- Add -> 'this' += 1
142143
-- Subtract -> 'this' -= 1
143144
--
144-
-- v :: Int -> 'View' Int Action
145-
-- v x = 'vfrag'
145+
-- v :: () -> Int -> 'View' Int Action
146+
-- v _ x = 'vfrag'
146147
-- [ H.button_ [ HE.onClick Add, HP.id_ "add" ] [ "+" ]
147148
-- , text (ms x)
148149
-- , H.button_ [ HE.onClick Subtract, HP.id_ "subtract" ] [ "-" ]
@@ -203,15 +204,15 @@
203204
-- ('+>')
204205
-- :: forall child model action a . Eq child
205206
-- => 'MisoString'
206-
-- -> 'Component' model child action
207+
-- -> 'Component' model () child action
207208
-- -> 'View' model a
208209
-- key '+>' vcomp = 'VComp' (Just (toKey key)) ('SomeComponent' vcomp)
209210
-- @
210211
--
211212
-- Practically, using this combinator looks like:
212213
--
213214
-- @
214-
-- view :: Int -> 'View' Int action
215+
-- view :: () -> Int -> 'View' Int action
215216
-- view x = 'div_' [ 'id_' "container" ] [ "counter" '+>' counter ]
216217
-- @
217218
--
@@ -233,7 +234,7 @@
233234
-- The 'App' type signature is a synonym for 'Component' 'ROOT'
234235
--
235236
-- @
236-
-- type 'App' model action = 'Component' 'ROOT' model action
237+
-- type 'App' model action = 'Component' 'ROOT' () model action
237238
-- @
238239
--
239240
-- 'ROOT' is a type tag that encodes a 'Component' as top-level. Which means it has no @parent@, hence we mark @parent@ as 'ROOT'.
@@ -244,6 +245,149 @@
244245
--
245246
-- 'startApp' and 'miso' will always infer @parent@ as 'ROOT'.
246247
--
248+
-- = Props
249+
--
250+
-- Inspired by [React props](https://react.dev/learn/passing-props-to-a-component),
251+
-- @miso@ allows a parent 'Component' to pass read-only data down to a child 'Component'
252+
-- via a mechanism called /props/ (short for /properties/).
253+
--
254+
-- == Props vs. Component-local state
255+
--
256+
-- * __model__: Component-local state. It is owned and mutated exclusively by the 'Component'
257+
-- itself through its 'Miso.Types.update' function. No other 'Component' can write to it directly.
258+
--
259+
-- * __props__: Data /inherited/ from the @parent@ 'Component'. Props flow downward through
260+
-- the component hierarchy and are read-only from the child's perspective. The parent decides
261+
-- what props to pass at mount time; the child cannot mutate them. Props that change in a
262+
-- the parent cause the child to re-render.
263+
--
264+
-- This mirrors the distinction in React between component state (@useState@) and props
265+
-- received from above (@function MyComponent({ name }) { ... }@).
266+
--
267+
-- === When to use props
268+
--
269+
-- Props are best suited for /metadata/ — contextual or configuration data that the child needs
270+
-- to know about but should not own. Good examples: a user's display name, a theme token, a
271+
-- locale string, or a read-only identifier used to customise rendering.
272+
--
273+
-- If the data drives the child's own business logic — counters it increments, form fields it
274+
-- edits, async state it manages — that data belongs in the child's @model@ instead. Putting
275+
-- mutable business-logic state in @props@ would require the parent to own and thread through
276+
-- every change, creating unnecessary coupling. Prefer @props@ for \"what the child should
277+
-- know\" and @model@ for \"what the child should do\".
278+
--
279+
-- == Props in 'Miso.Types.view'
280+
--
281+
-- The 'Miso.Types.view' field of a 'Component' always takes @props@ as its first argument:
282+
--
283+
-- @
284+
-- view :: props -> model -> 'View' model action
285+
-- @
286+
--
287+
-- Top-level applications have no parent, so @props@ is always @()@:
288+
--
289+
-- @
290+
-- view :: () -> model -> 'View' model action
291+
-- view _props model = …
292+
-- @
293+
--
294+
-- == Props in 'Effect' \/ 'Miso.Types.update'
295+
--
296+
-- Use 'getProps' inside the 'Effect' monad to read the current value of @props@:
297+
--
298+
-- @
299+
-- update :: Action -> 'Effect' parent props Model Action
300+
-- update = \\case
301+
-- SomeAction -> do
302+
-- p <- 'getProps'
303+
-- 'io_' ('consoleLog' (ms (show p)))
304+
-- @
305+
--
306+
-- Alternatively, use the 'Miso.Lens.view' combinator with the 'props' lens:
307+
--
308+
-- @
309+
-- update = \\case
310+
-- SomeAction -> do
311+
-- p <- 'Miso.Lens.view' 'props'
312+
--
313+
-- @
314+
--
315+
-- == 'ROOT' — the top-level 'Component'
316+
--
317+
-- When a 'Component' is passed to 'startApp' (or 'miso') it has no parent.
318+
-- The @parent@ type is specialized to 'ROOT' and @props@ is fixed to @()@:
319+
--
320+
-- @
321+
-- type 'App' model action = 'Component' 'ROOT' () model action
322+
-- @
323+
--
324+
-- Because there is no parent to inherit from, @props@ will always be @()@ for a
325+
-- root-level 'Component'. You can simply ignore the first argument in 'view' and
326+
-- skip 'getProps' in 'Miso.Types.update'.
327+
--
328+
-- == Passing props to a child 'Component'
329+
--
330+
-- Use 'mountWithProps_' (keyed) or 'mountWithProps' (unkeyed) in the parent's 'view' to
331+
-- mount a child and supply its props:
332+
--
333+
-- @
334+
-- 'mountWithProps_'
335+
-- :: ('Eq' child, 'Eq' props)
336+
-- => 'MisoString'
337+
-- -> props
338+
-- -> 'Component' parent props child action
339+
-- -> 'View' parent a
340+
-- @
341+
--
342+
-- == Example: child reading parent-supplied props
343+
--
344+
-- The following shows a parent 'Component' that maintains a greeting string in its
345+
-- @model@ and passes it as @props@ to a child 'Component'. The child renders the
346+
-- greeting and can also read it from within its 'Miso.Types.update' function.
347+
--
348+
-- @
349+
-- -----------------------------------------------------------------------------
350+
-- -- The props type: what the parent shares with the child
351+
-- newtype Greeting = Greeting 'MisoString' deriving ('Eq')
352+
-- -----------------------------------------------------------------------------
353+
-- -- Child component
354+
-- --
355+
-- -- parent props model action
356+
-- -- | | | |
357+
-- child :: 'Component' ParentModel Greeting () ChildAction
358+
-- child = 'vcomp' () updateChild viewChild
359+
-- where
360+
-- viewChild :: Greeting -> () -> 'View' () ChildAction
361+
-- viewChild (Greeting g) _ =
362+
-- 'div_' [] [ 'text' ("Hello, " <> g <> "!") ]
363+
--
364+
-- updateChild :: ChildAction -> 'Effect' ParentModel Greeting () ChildAction
365+
-- updateChild = \\case
366+
-- ReadGreeting -> do
367+
-- Greeting g <- 'getProps'
368+
-- 'io_' ('consoleLog' g)
369+
-- -----------------------------------------------------------------------------
370+
-- -- Parent component: owns the greeting, passes it to the child as props
371+
-- parent :: 'App' ParentModel ParentAction
372+
-- parent = 'vcomp' (ParentModel "World") 'noop' viewParent
373+
-- where
374+
-- viewParent :: () -> ParentModel -> 'View' ParentModel ParentAction
375+
-- viewParent _ (ParentModel g) =
376+
-- 'mountWithProps_' "child" (Greeting g) child
377+
-- -----------------------------------------------------------------------------
378+
-- newtype ParentModel = ParentModel 'MisoString' deriving ('Eq')
379+
-- data ChildAction = ReadGreeting
380+
-- data ParentAction
381+
-- @
382+
--
383+
-- A few things to notice:
384+
--
385+
-- * The child's @parent@ type parameter is @ParentModel@. This must match the
386+
-- parent 'Component' @model@ type — 'mountWithProps_' enforces this at compile time.
387+
-- * 'getProps' inside the child's 'Miso.Types.update' yields a @Greeting@, not
388+
-- the full @ParentModel@. The child only sees what the parent explicitly chose to share.
389+
-- * The root 'App' always has @props ~ ()@; no extra plumbing is needed when calling 'startApp'.
390+
--
247391
-- = 'VComp' lifecycle hooks
248392
--
249393
-- 'Component' are mounted on the fly during diffing. All t'Component` are equipped with `mount` and `unmount` hooks. This allows the defining of custom actions that will be processed in response to lifecycle events.
@@ -271,7 +415,7 @@
271415
--
272416
-- data Action = Highlight DOMRef
273417
--
274-
-- update :: Action -> 'Effect' parent model Action
418+
-- update :: Action -> 'Effect' parent props model Action
275419
-- update = \\case
276420
-- Highlight domRef -> 'io_' $ do
277421
-- ['js'| hljs.highlight(${domRef}) |]
@@ -344,7 +488,7 @@
344488
-- , 'li_' [ 'key_' "key-2" ] [ "b" ]
345489
-- , "key-3" '+>' counter
346490
-- , 'textKey' "key-4" "text here"
347-
-- , vfrag_ "key-5" [ "foo", "bar" ]
491+
-- , 'vfrag_' "key-5" [ "foo", "bar" ]
348492
-- ]
349493
-- @
350494
--
@@ -420,7 +564,7 @@
420564
-- The 'Effect' type is defined as a 'RWS'.
421565
--
422566
-- @
423-
-- type 'Effect' parent model action = 'RWS' ('ComponentInfo' parent) ['Schedule' action] model ()
567+
-- type 'Effect' parent props model action = 'RWS' ('ComponentInfo' parent props) ['Schedule' action] model ()
424568
-- @
425569
--
426570
-- * The 'Control.Monad.Reader' portion of 'Effect' is 'ComponentInfo'. 'ask', 'asks', 'Miso.Lens.view' can be used to access its fields.
@@ -587,7 +731,7 @@
587731
--
588732
-- import Miso.FFI.QQ ('js')
589733
--
590-
-- update :: Action -> 'Effect' parent model Action
734+
-- update :: Action -> 'Effect' parent props model Action
591735
-- update = \\case
592736
-- Log msg -> io_ [js| console.log(${msg}) |]
593737
--
@@ -669,7 +813,7 @@
669813
-- A simple example of static prerendering would be an @index.html@ page with some HTML
670814
--
671815
-- @
672-
-- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>" > index.html
816+
-- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>\<html\>" > index.html
673817
-- @
674818
--
675819
-- And a miso application that looks like:

0 commit comments

Comments
 (0)