jorgebucaran / hyperapp

1kB-ish JavaScript framework for building hypertext applications
MIT License
19.08k stars 780 forks source link

Modules in V2 #896

Closed zaceno closed 3 years ago

zaceno commented 5 years ago

πŸ’β€β™‚ Check out the current proposal here.


Consider this minimal example of a modular Elm app (copied from here):

Button.elm ```elm module Button exposing (Model, Msg(..), init, update, view) import Html exposing (Html, button, div, h1, text) import Html.Events exposing (onClick) -- MODEL type alias Model = Int init : Model init = 0 -- UPDATE type Msg = Increment | Decrement update : Msg -> Model -> Model update msg model = case Debug.log "Button.update msg: " msg of Increment -> model + 1 Decrement -> model - 1 view : Model -> Html Msg view model = div [] [ button [ onClick Decrement ] [ text "-" ] , div [] [ text (String.fromInt model) ] , button [ onClick Increment ] [ text "+" ] ] ```
Main.elm ```elm type alias Model = { button : Button.Model } init : Model init = { button = Button.init } -- UPDATE type Msg = ButtonMsg Button.Msg update : Msg -> Model -> Model update msg model = case Debug.log "update msg" msg of ButtonMsg buttonMsg -> { model | button = Button.update buttonMsg model.button } -- VIEW view : Model -> Html Msg view model = div [] [ h1 [] [ text "elm button example" ] , map ButtonMsg (Button.view model.button) ] ```

Notice two things:

A) Increment and Decrement are completely unaware of the shape of the global app state. Their definitions are completely self contained.

B) Main.elm never once explicitly references Increment or Decrement.

As far as I've been able to tell, this is not possible to achieve using Hyperapp 2. You can have A or B but not both. This has frightening implications for large scale apps.

I would like to see a convention, add-on library or core change -- or some combination of all three -- which enabled us to replicate the example above in Hyperapp v2.

That essentially means being able to use a module which defines its actions in a self-contained, app-agnostic way, without being required to export them. They should not need to be explicitly referenced anywhere else.

Additionally, whatever we come up with, I would also want it to work in a recursive fashion, that is to say: a module can use a module which uses modules ...

EDIT: Also, of course, this shouldn't just apply to actions dispatched from views. It should be the same for actions dispatched from Effects (whatever action initiated the effect), and subscriptions.

jorgebucaran commented 5 years ago

Here's how this could look like for Hyperapp.

First, define your button module:

import { h } from "hyperapp"

const Increment = state => state + 1
const Decrement = state => state - 1

export const init = 0
export const view = state =>
  h("div", {}, [
    h("button", { onclick: Decrement }, "-"),
    h("div", {}, state),
    h("button", { onclick: Increment }, "+")
  ])

In your main app, import the button and a new core function, e.g., map, that transforms all messages generated inside Button.view to something that makes sense for your main state.

import { h, app, map } from "hyperapp"
import * as Button from "./Button"

app({
  init: {
    button: Button.init()
  },
  view: state =>
    h("div", {}, [
      h("h1", {}, "Here's a button"),
      map(state => ({ button: state }), Button.view(state.button))
    ]),
  node: document.getElementById("app")
})
zaceno commented 5 years ago

Another example, using the same module twice. I had to ask in the Elm-forum because it wasn't clear to me at the time. In hindsight, I realized I had misunderstood parts of how consuming a module works in elm and this cleared things up for me at least: https://discourse.elm-lang.org/t/using-multiple-instances-of-modules/4578

jorgebucaran commented 5 years ago

@zaceno How would you translate that into Hyperapp using what's currently proposed in https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547380787?

zaceno commented 5 years ago

@jorgebucaran It doesn't really change anything about your proposal. If I read it correctly (and understand what Elm's map does), it is basically:

map(actionTransformer, vnodes) -> mappedVnodes

Any onclick: Foo in vnodes will be transformed into onclick: MappedFoo in mappedVnodes, where MappedFoo is produced from actionTransformer(Foo).

Right?

zaceno commented 5 years ago

This approach requires no implementation in core and is so straightforward that I've certainly tried it before. But I can't recall what the problem was.


const namedScope = name =>
    action =>
        (state, payload) =>
            ({...state, [name]: action(state[name], payload)})

const Counter = scope => {
    const init = 0
    const incr = scope(state => state + 1)
    const decr = scope(state => state - 1)
    const view = state => (
        <p>
            <button onclick={decr}>-</button>
            {state}
            <button onclick={incr}>+</button>
        </p>
    )
    return {init, view}
}

const Main = (() => {

    const A = Counter(namedScope('A'))
    const B = Counter(namedScope('B'))

    app({
        init: {A: A.init, B: B.init},
        view: state => (
            <body>
                <h1>Here's two counters</h1>
                {A.view(state.A)}
                {B.view(state.B)}
            </body>
        ),
        node: document.body,
    })

})()

EDIT IV: removed edits II - III , since I don't think they added much to the discussion and make you scroll a lot πŸ˜›

sergey-shpak commented 5 years ago

Modules Implementation (I haven't tried this, only for the discussion purposes) As usual, I tried to follow requirements:

  1. simple and fits into current API (without any significant/complex core changes)
  2. scalable and doesn't limit any functionality (dispatching 'sliced' or any normal action/effect at will)

Map. Since module actions are not exported directly, we can use vdom props to get the action references and decorate them, so map function becomes simple as

import * as Button from 'button'

const decorate = (vdom, getter, setter) => ({
  ...vdom,
  // recursive child decoration
  childrens: vdom.childrens.map(node => 
    decorate(node, getter, setter)
  ),
  props: Object.keys(vdom.props).reduce((obj, key) => {
    obj[key] = !key.startsWith('on')
    ? vdom.props[key]
    // wrapping module action with custom 'filter'
    : state => [
      vdom.props[key], 
      function filter(param){
        // and yes, filter which returns 'slice' function
        // 'param' is also available and can be passed 
        return function slice(obj){
          return obj ? setter(state, obj) : getter()
        }      
    }]
    return obj
  }, {})
})

const map = (fn, getter, setter) => 
  decorate(fn(getter()), getter, setter)

app({
  init: { button: Button.init },
  view: state => <div>
    map(
      Button.view, 
      () => state.button, 
      (state, button) => ({ ...state, button })
    )
  </div>
})

Button.

const increment = (state, slice) => {
  // now you have plenty options here
  // 1. you can call `slice()` to get sliced state (map getter)
  // 2. you can `return slice(2)` to set sliced state (map setter)
  // 3. you can dispatch sliced effect
  // like `return [slice(1), effect()]` or even passing slice further
  // like `return [slice(1), effect({ action: [ action, () => slice ] })]`
  // 4. at the same time you can change global state if needed
  // like `return { ...state, some: 'prop' }`
  // 5. or dispatch any not mapped (external) action
  // like `return [location, '/path']`
  return slice(2)
}
export const init = 0
export const view = state => h('button', {
  onclick: increment
}, state)

I haven't tried this on working example, but I can imagine this working. @jorgebucaran @zaceno any thoughts?

zaceno commented 5 years ago

@sergey-shpak looks like a good possibility but how would you do it with payloads? And why the getter and setter? Why not just a single action transform?

sergey-shpak commented 5 years ago

@zaceno there are some pros and cons with this pattern, one of them that filters (~payloads~) inside of module should pass 'slice' function into action (also mentioned in comments but haven't implemented it yet - 'slice' filter also can pass param(payload) if any)

Since we can call sliced effects, getters and setters are used to return latest state, but I have to think about it more if I can simplify it

In general, main idea of this approach is quite simple - passing 'slicer' function as parameter into each module action, there are also some variations for dev userland

const action = (state, slicer) => {
  const slice = slicer(state) // get sliced state
  return slicer(state, 1) // set sliced state
}
jorgebucaran commented 5 years ago

@zaceno @sergey-shpak This feature must be in core.

zaceno commented 5 years ago

@jorgebucaran

This feature must be in core.

I'm not disagreeing, but could you clarify: Do you mean it is something you don't want to leave up to userland, or are you aware of a technical reason why it must be in core?

Here's a possible implementation of map outside core:

const map = (f, vnode) => ({
    ...vnode,  
    props: Object.entries(vnode.props)
        .map(([key, val]) => [key, key[0] === 'o' && key[1] === 'n' ? f(val) : val])
        .reduce((o, [key, val]) => ({...o, [key]: val}), {}),
    children: vnode.children.map(child => map(f, child))
}) 

It will impact performance the more it is used, but I don't know of a way to avoid that by implementing it in core.

zaceno commented 5 years ago

@sergey-shpak Is your solution really much different than https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547662315 ? You make sure that actions get the slicer as an argument. I just make the slicer available in the entire scope of the module.

Also: what are the pros/cons of that approach?

sergey-shpak commented 5 years ago

@zaceno we are working on the same task some approaches can look similar, pros of https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547857416 are:

cons:

jorgebucaran commented 5 years ago

@zaceno Do you mean it is something you don't want to leave up to userland, or are you aware of a technical reason why it must be in core?

Don't want to leave up to userland. No technical reason comes to mind, either. Well, maybe that it'd be hard to implement in userland?

zaceno commented 5 years ago

@jorgebucaran πŸ‘

Well, maybe that it'd be hard to implement in userland?

Doesn't look like it. At least, the code I pasted above seems to do the trick. See this codepen where I use it: https://codepen.io/zaceno/pen/gOOGzPr

jorgebucaran commented 5 years ago

@zaceno Can we match this API https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547380787?

And does this solution look efficient to you?

mshgh commented 5 years ago

Here is my idea how to make usage of modules for user less repetitive and error prune. I am picking code from @jorgebucaran as an example only. The same approach can be used for other proposals I can see here too, I think.

the original code ```javascript import { h, app, map } from "hyperapp" import * as Button from "./Button" app({ init: { button: Button.init() }, view: state => h("div", {}, [ h("h1", {}, "Here's a button"), map(state => ({ button: state }), Button.view(state.button)) ]), node: document.getElementById("app") }) ```

I do not propose any change to the implementation. Just to make it easier for the end-user and eliminate all code which can be auto-generated.

import { h, app } from "hyperapp"
import * as Button from "./Button"

const button = _mapping_("button", Button);

app({
  init: {
    ...button.init() // returns { "button": 0 }
  },
  view: state =>
    h("div", {}, [
      h("h1", {}, "Here's a button"),
      button.view // the same as `map(state => ({ button: state }), Button.view(state.button))`
    ]),
  node: document.getElementById("app")
})
more complex example ```javascript import { h, app } from "hyperapp" import * as Button from "./Button" import _ from "lodash" const button1 = _mapping_("buttons.button1", Button); const button2 = _mapping_("buttons.button2", Button); app({ init: _.merge({}, button1.init(), button2.init()), // we need deep merge here view: state => h("div", {}, [ h("h1", {}, "Here's a button"), h("div", { onclick: button1.increment }, "Use button API"), // assuming Increment() is exported h("div", {}, [ "Use another view of button", button2.viewCount // e.g. present counter value only ]), button1.view, button2.view, ]), node: document.getElementById("app") }) ```
zaceno commented 5 years ago

@jorgebucaran

Can we match this API #896 (comment)?

If you're referring to state => ({ button: state }) to describe the action transform then no, I don't think we can. I'm not even sure how to understand what that means. I thought maybe it was some kind of mistake, and you meant to describe an action transform.

At least how I understand Elm's map works is that it just applies a given function to all messages in the vdom. Nothing deeper or more magical. In theory that means you could (in hyperapp equivalent):

map(action => "foo", HomePage.view(state.page))

... and whenever anyone clicked a button in the home-page, the global state would be set to "foo" (and the app would crash, so you wouldn't do that of course... πŸ˜› ... but you could!)

Using my map implementation in the codepen above, the example could become:

import { h, app, map } from "hyperapp" // <--- map applies transform to all on* vals in a vnode
import * as Button from "./Button"
import slice from '@hyperapp/slice'  // <--- make action transforms based on "slices"

app({
  init: {
    button: Button.init  // <---- was Button.init() before. That was a mistake I assume
  },
  view: state =>
    h("div", {}, [
      h("h1", {}, "Here's a button"),
      map(slice('button'), Button.view(state.button))
    ]),
  node: document.getElementById("app")
})
jorgebucaran commented 5 years ago

I'd love to hear @SkaterDad's feedback on this one too.

zaceno commented 5 years ago

Further on the topic of Elm's map, here's the example from their docs:

type Msg = Left | Right

view : model -> Html Msg
view model =
  div []
    [ map (\_ -> Left) (viewButton "Left")
    , map (\_ -> Right) (viewButton "Right")
    ]

viewButton : String -> Html ()
viewButton name =
  button [ onClick () ] [ text name ]

In hyperapp terms, this would look like:

const Left = state => ...
const Right = state => ...

const Button = name => h('button', {onclick: null}, name)

app({
  ...
  view: state => h('div', {}, [
    map(_ => Left, Button('Left')),
    map(_ => Right, Button('Right')),
  ])
})

This is obviously a bit contrived, but I can think of a few other ways this could be useful than just for modularization.

zaceno commented 5 years ago

@mshgh

...
h("h1", {}, "Here's a button"),
button.view // <--- don't you need state here?
...
zaceno commented 5 years ago

@sergey-shpak Here's your (I think) version of slices using my map implementation. As a demonstration of the power of allowing a generic action transform:

https://codepen.io/zaceno/pen/eYYGwZB?editors=0010

jorgebucaran commented 5 years ago

It's impressive that this can be achieved in userland. What about actions that return effects? Does your solution cover those? It does!

sergey-shpak commented 5 years ago

@zaceno looks good, and more important it works, I would suggest to use startsWith method to simplify 'onEvent' check, but I'm quite happy with sergeySlice :smile: I can see two pitfalls here

jorgebucaran commented 5 years ago

Now that we're here, we might as well go for something like this:

https://codepen.io/jorgebucaran/pen/JjjOjwB?editors=0010

import { Counter } from "./Counter.js"
import { component } from "./component"

const [A, B] = component({ A: Counter, B: Counter })

app({
  init: {
    ...A.init,
    ...B.init
  },
  view: state => (
    <div>
      <h1>Reusable counters:</h1>
      <A.view state={state.A} />
      <B.view state={state.B} />
    </div>
  ),
  node: document.getElementById("app")
})
sergey-shpak commented 5 years ago

@jorgebucaran @zaceno even though I proposed passing 'slice' into actions, and this pattern has some advantages, but I believe there is always a room for perfection, so I decided to take another try on this task... and now I'm quite happy with what I got

I call it callable state/context Main idea is passing component state with possibility to wrap component actions (that is why it's named callable). Implementation based on extending wrapper function with component state. It is quite simple and scalable at the same time.

Component (as simple as possible)

const multiply = (ctx, by) => 
  ({ ...ctx, value: ctx.value * by }) // or even `effect(ctx(action))`

const Button = {
  init: {
    value: 1
  },
  view: (ctx, child) => 
    h('button', { onclick: [ctx(multiply), 3] }, 
      `multiply state by 3 = ${ctx.value}`)
}

App

import { app, h, wire } from 'hyperapp'

app({
  init: { 
    buttonA: Button.init,
    buttonB: Button.init,
  },
  view: state => h('div', {}, [

    wire(
      Button.view,
      (state) => state.buttonA,
      (state, buttonA) => ({ ...state, buttonA })
    )(state),

    wire(
      Button.view,
      (state) => state.buttonB,
      (state, buttonB) => ({ ...state, buttonB })
    )(state)

  ]),
  node: document.getElementById('app')
})

and wire implementation (shipped as part of core, like Lazy, etc)

const wire = (fn, getter, setter) => 
  state => fn(Object.assign(
    function ctx(action){
      return (state, param) => 
        setter(state, action(ctx, param))        
    },
    getter(state)
  ))

and working example https://codepen.io/sergey-shpak/pen/MWWvEPe

zaceno commented 5 years ago

@jorgebucaran

What about actions that return effects? Does your solution cover those?

It does not. I've been trying to research and think about precisely that problem and not sure if I'm getting more or less confused ...

Looking at the the Elm realworld example here, it seems they use Cmd.map in the main update, to do essentially the same thing as Html.map but for commands. I'm not exactly sure how to translate that to Hyperapp though.

EDIT: PS: The docs strongly discourage the use of both Html.map and Cmd.map, yet Feldman uses them in the Main.elm of his SPA-example, so... I don't know what's up with that.

About Cmd.map the docs say:

This is very rarely useful in well-structured Elm code, so definitely read the section on structure in the guide before reaching for this!

... and the structure page links to the elm-spa-example wherein is found... Cmd.map πŸ˜†

EDIT II: In trying to research this, I'm finding myself playing with Elm code again, and surprisingly I'm having a much less traumatizing an experience this time around!

zaceno commented 5 years ago

@jorgebucaran @sergey-shpak Seeing your recent experiments, I need someone to remind me: what is the problem with this again: https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547662315 ?

sergey-shpak commented 5 years ago

@zaceno there are no problems as for me, in general it works as expected. Imo, the question is how it works, how much difference it creates between usual and reusable components/modules, how much additional code user should write to achieve this and how readable it is, how to test 'mapped' actions, etc

jorgebucaran commented 5 years ago

@zaceno It does not. I've been trying to research and think about precisely that problem and not sure if I'm getting more or less confused...

Hmm, I should've tried it first. I just looked at your resolve function and thought you were handling effects.

const resolve = (action, filter) =>
  filter && !Array.isArray(action)
    ? [action, filter]
    : !filter
    ? resolve(action, id)
    : resolve(
        action[0],
        compose(
          fix(action[1]),
          filter
        )
      )
zaceno commented 5 years ago

@jorgebucaran

Sorry I misread your question first and gave a non-sequitor response.

Here's the more correct response:

My map impl doesn't care wether actions return effects or not, and doesn't need to. What to do if an action returns an effect should be handled by the user in the transform that goes in to map. (Just like how it's up to a user how to handle ButtonMsg in the update of Main.elm).

So, handling effect-returns should be done in my slice implementation which is a separate thing (a helper for defining an action transform). I could do that but didn't feel it necessary since I don't use effects yet.

What I thought you meant was how to handle when effects dispatch actions. That is why I brought up Cmd.map.

jorgebucaran commented 5 years ago

What I thought you meant was how to handle when effects dispatch actions. That is why I brought up Cmd.map.

We need to find a way to attach the transform information to actions, not vnode props to solve this.

zaceno commented 5 years ago

@jorgebucaran

We need to find a way to attach the transform information to actions

I'm not 100% sure we want this forced on effects just because we used map. If we want the same flexibility Elm has, I think we should leave it up to the user wether or not they want actions-dispatched-from-effects mapped with the same transform that was applied to the action that returned the effect.

The way we could leave it up to the user is to transform the effects inside the action transform passed to map. (Since technically it is the user's responsibility to define that transform, even though we may provide conventional helpers). Now that I think of it, that actually seems to parallell quite a lot with how Feldman uses the Cmd.map in the update of Main.elm in his elm-spa-example.

... im going to go try that and see how it works.

mshgh commented 5 years ago
...
h("h1", {}, "Here's a button"),
button.view // <--- don't you need state here?
...

@zaceno you are correct. I went too far with the simplification. Should have been button.view(state)

zaceno commented 5 years ago

@jorgebucaran @frenzzy et al

Here's a new version. I added a function mapEffect corresponding to Cmd.map in Elm, and I use it in my new and improved slice mapper. Now dispatching mapped effects works as well. Have a look!

https://codepen.io/zaceno/pen/zYYPNrN?editors=0010

jorgebucaran commented 5 years ago

@mshgh I went too far with the simplification. Should have been button.view(state)

That's not enough, you need to map the view to the "slice". Like this https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-548152322

@zaceno Have a look!

Done. It works! Very nice. Okay, next is:

1) How do we bring this into core? Can it be implemented in less code? 2) How is the API going to look? 3) Subscriptions?

zaceno commented 5 years ago

@jorgebucaran

  1. How do we bring this into core? Can it be implemented in less code?

In less code, sure maybe. But I'm not sure any of this needs to be in in the actual src/index.js - rather I feel like maybe it belongs in a separate @hyperapp/vdom (with Lazy perhaps) or @hyperapp/module or something.

If we really want it to be less code, I suppose there is some possibility of code reuse in src/index.js but I don't see any necessity or apparent benefit of deeper integration with the main app engine.

  1. How is the API going to look?

Well, my thought is: I think it's important that map or eventMap or vdomMap or whatever we end up calling it, completely unopinionated about how to transform actions. It just takes a transform as an argument.

That means it is up to users to define their own transforms, which on the other hand can be kind of hard. So I think it makes sense to export something like my slice transform as a helper, since that is presumably a common need. Akin to how we export official effects / subs.

This all means I think we could/should have a @hyperapp/something which exports at least three functions:

mapEvents: (actionTransform, vdom) -> vdom mapEffect: (actionTransform, effect) -> effect slice: string -> actionTransform

  1. Subscriptions?

Good question! πŸ˜… ...next order of business, I guess!

jorgebucaran commented 5 years ago

@zaceno It's built into Elm, though!

zaceno commented 5 years ago

@jorgebucaran

It's built into Elm, though!

Right. Well it's exported from a non-core package, but that export relies on support in the virtual-dom package. I wasn't able to follow the implementation entirely, but it looks like we could/should have a "map" or "tag" property on vnodes, which we, on render, propagate down through the vnode tree.

Also, I just realized mapped subscriptions will require built in support as well, because applying transforms to them directly will make them new instances each render.

mshgh commented 5 years ago

@mshgh I went too far with the simplification. Should have been button.view(state)

That's not enough, you need to map the view to the "slice". Like this #896 (comment)

@jorgebucaran you do not need to. It was actually part of my point how to make usage less error-prune. You are referring to your code <A.view state={state.A} /> and I say this can be simplified to <A.view state={state} /> provided you never want to do something like <A.view state={state.B} />

Checking your codepen line 39 should change from view: ({ state }) => map(slice, view(state)) to view: ({ state }) => map(slice, view(state[key])) - tried, works.

jorgebucaran commented 5 years ago

@zaceno It's true that Elm uses an external package for this! Perhaps we should export Lazy and map (name still subject to change) into @hyperapp/html.

@mshgh I get your point, and we could get away with it in Hyperapp, because unlike Elm, we're not mapping typed messages, and just composing functions, passing data around. Still, I prefer state.A or state.B because explicitness is good.

zaceno commented 5 years ago

@jorgebucaran sounds good! FYI I’m currently working on integrating support in core. It’s tricky but doable and will be more efficient than my userland solution.

zaceno commented 5 years ago

Made a PR! Here's a demo of it working: https://codesandbox.io/s/hyperapp-action-mapping-demo-z3ydz

zaceno commented 5 years ago

New example demonstrating how child-to-parent communication could work, using (what I think is) our equivalent of the translator pattern

Here's the sandbox: https://codesandbox.io/s/hyperapp-action-mapping-with-translator-pattern-demo-cudh3

Note this changes nothing about the necessary plumbing needed in hyperapp (in the PR above). I just made my slice function a little more elaborate.

mshgh commented 5 years ago

@zaceno I took your first demo and added one more layer to support components the way I like :-) see this. I didn't change anything just re-used your slice*() methods and turned components (Counter and Main) into objects. I've noticed your other demo only now - didn't see it yet. P.S. I do not know how to use your functions to support actions, so I added my own way which is probably too simplistic...

jorgebucaran commented 5 years ago

@zaceno We might never agree on an API for modules, so what is the minimal Hyperapp "primitive" I can give you to help you assemble your "slice.js" module the way you want?

zaceno commented 5 years ago

@jorgebucaran

so what is the minimal Hyperapp "primitive" I can give you to help you assemble your "slice.js" module the way you want?

The absolute bare minimum of help would be to merge my PR -- that was the idea of the PR. To just be the plumbing. You could even skip the mapEvents, mapEffect, mapSubs functions if you want -- but then users would have to rely on knowing the "secret" of how to map actions (add an actionMap prop to vnode, or add a third element to effect-tuples)

zaceno commented 5 years ago

@mshgh Cool, but there seems to be some mistake since only A is affected by actions (and both counters render A) (on my phone atm so I couldn’t easily check the error)

mshgh commented 5 years ago

Interesting. Works fine for me. Both updates to A as well to B including the delayed reset for decrements. Chrome 77.0.3865.120 Win10

zaceno commented 5 years ago

@mshgh You're right it does work -- must have just been my phone. Safari has been weird lately

SkaterDad commented 5 years ago

Haven't had much time to work w/ Hyperapp lately and review this.

I tend to have a fairly flat state tree w/ V2, and pass in a name/key for the component state. It's simple and gets the job done. You have to name the state no matter what, and this is only a little extra work. My production apps are still in V1 due to time constraints, so this is just experimental.

I kind of like the idea that every action can see the full app state. You never know when that will come in handy, so I'm hesitant to create isolated components as I explore V2.

Tedious example code below:

```js // App const state = { // ...other stuff widget1: { value: "I'm the first one." }, widget2: { value: "I'm the 2nd one." } } const view = state => (
) // Widget.js function WidgetAction(state, { name, value }) { // of course, you could extract this to a helper return { ...state, [name]: { ...state[name], value: value } } } function Widget({ name, value }) { return } ```

I actually really like what @zaceno posted here for simple components. It's basically what I do, but better abstracted. https://github.com/jorgebucaran/hyperapp/issues/896#issuecomment-547662315