anthonyshort / deku

Render interfaces using pure functions and virtual DOM
https://github.com/anthonyshort/deku/tree/master/docs
3.41k stars 130 forks source link

Abstraction of things that call setState #218

Closed ashaffer closed 8 years ago

ashaffer commented 9 years ago

Is there an accepted pattern for doing this? My motivating example is the form below:

function render(component, setState) {
  const {state} = component
  const {dirty, user, submitted} = state

  const {valid, errors} = validateUser(user)
  const errMsgs = userMessages(errors)

  return (
    <form>
      <SignupInput placeholder='FULL NAME' onChange={setField('name')} error={getError('name')} />
      <SignupInput placeholder='EMAIL' onChange={setField('email')} error={getError('email')} />
      <SignupInput type='password' placeholder='PASSWORD' onChange={setField('password')} error={getError('password')} />
    </form>
  )

  function setField (field) {
    return function(element) {
      const value = element.delegateTarget.value
      setState({
        user: assign({}, user, {[field]: value}),
        dirty: assign({}, dirty, {[field]: true})
      })
    }
  }

  function getError (field) {
    return (submitted || dirty[field]) && errMsgs[field]
  }

It is a simple signup form with validation and a model, and i'd like to be able to abstract out setField, getError, and the little bit of validation setup in render, but setField's dependence on the setState function is making things complicated.

The solutions I can think of are:

The problem with all of them to varying degrees is that they make it less clear where state is being manipulated (which I suppose may just be an inherent tradeoff of abstraction that can't be eliminated).

anthonyshort commented 9 years ago

Yeah I'm probably going to add setState in as a separate module if possible by letting the user define what the 'handler' function is that is passed as the second param to every render function. This is mostly to make migrating to the new API easier, I think local state is something we probably wouldn't want to encourage.

joshrtay commented 9 years ago

Sounds good. That keeps it simple.

ashaffer commented 9 years ago

@joshrtay Ya, I think the argument against using component id's is that that would enforce a de-facto isolated local state. If you don't give components ids, you force the state locality to be expressed in the semantic domain of the application, which allows you to access it from other places if you need to.

Consider what I think is probably a bit of a pathological case for the no-local-state approach: A feed with dropdown menus on each feed item. It seems like the open/closed state should just be local to the dropdown, and that is how almost everyone implements it. But this totally breaks down in at least two pretty common cases:

In the local state approach, this requires you to propagate that information around your application in various ugly and complex ways. Without local state, you'd be forced to localize the open/closed bool in some way that is native to your application, which should hopefully make accessing state in remote places a bit more natural. I think, maybe.

anthonyshort commented 9 years ago

That's a really great example of how local state breaks things. I'm going to include that in the docs :)

ashaffer commented 9 years ago

In thinking about it more, the event bubbling concept is probably unnecessary. You can just pass functions down in props to emulate it anyway, and then the linkages are all explicit, and there are no new abstractions.

anthonyshort commented 9 years ago

^ Yup!

<App handler={ a => handler('child', a) } />

It's how Elm does it. Now Deku can focus on just rendering those components and calling hooks when needed. Components from React, architecture of Elm, simpler than virtual-dom.

The approach that the elm/snabbdom people seem to be taking is that you just pass everything down and it's really not as bad as it seems. That seems plausible to me, but maybe we should try to come up with some strategies/patterns for implementing that in vaguely real-world scenarios?

Elm handles this by just passing down "context" objects too then you can just keep forwarding on or modifying as you need it. You could just use props for that too. It's not magic like React, but that's probably a good thing. The context would usually only be passed down through the components that are coupled to your app anyway.

I like the idea of finishing this off, pushing 1.0, and providing a lot of solid documentation and guides for these more complex cases people eventually run into.

ashaffer commented 9 years ago

Ya, context objects passed down in props seems like the right approach to me. You could even do that with the dispatch function too, there's no real reason it needs to be special. <App state={state}, dispatch={dispatch} />.

It seems like the main argument for giving dispatch a privileged place in the API would be so that you could share components that call it. But perhaps its better to say that reusable components just shouldn't be dispatching directly?

If you go that route, it seems to me 1.0 is done :).

As for docs, I would really love to see a curated zoo of corner-cases and difficult real-world problems implemented in elegant ways that the community can learn from and iterate on together. It'd be a fair bit of work, but it's probably something we could all add to as we're developing applications with deku and we come across interesting/instructive patterns.

ashaffer commented 8 years ago

@anthonyshort I think i've encountered a class of things that may be awkward with this approach. Some data is extremely global and used by small components in a way that is essentially impossible to predict from knowledge of their parents.

One example is say, the current window size. Something like a <Figure> component may want that information, but it will be deeply nested in some other components, and propagating that information down through all of the other components seems sort of arbitrary and awkward, because it isn't inherently a component of the parent's state, it seems like.

Another, I have a set of components that render images, and they like to wait until the image is loaded (in the browser's cache) before rendering them. On my global state atom i'd like to have a map of {<url>: <loaded>}, which all of these components may examine to decide if they should render their image yet or not. Since it is impossible for the parent components to know which images the children will want to load in this way, i'd have to propagate the entire map down to all of the children that may or may not render this type of component.

Both of these cases and other things like them seem like maybe they are just inherently global chunks of data? Or can we come up with some pattern for propagating these types of things that doesn't feel so janky?i The thing that springs to mind for me here is just to retain the sources abstraction deku currently has, so that at the top-level you can define a select few pieces of data that are accessible to everyone, say (in my case): state, windowDimensions, and loadedImages, and presumably a few other highly global pieces of data, with the knowledge that these things should be used as sparingly as possible.

anthonyshort commented 8 years ago

@ashaffer I was thinking about some of that. Things like Redux and routers might want to put something on a context to pass it all the way down. We could support a single context that the whole tree has access to which would be extremely simple. I'm not sure of the benefits of having nested contexts, so maybe the simple approach would cover most use-cases.

export function render (model, context) {
  return <div></div>
}

You could always just pass a context object down yourself too which would be more explicit and less magic-y.

export function render (model) {
  return <SomeComponent context={model.context} />
}
ashaffer commented 8 years ago

Ya I think a single global context might be the right approach. If components don't have their own state, there is no need for nested contexts anyway. Feels like the right balance of simplicity and power.

anthonyshort commented 8 years ago

If components don't have their own state, there is no need for nested contexts anyway.

That makes so much sense! :D Nice.

troch commented 8 years ago

I agree with that. For a while I thought about making my router architecture work with nested routers. Why would you want that if you can nest routes? Same goes with components, why would you want to nest contexts when you can nest stateless components. I don't know if that makes sense :foggy:

ashaffer commented 8 years ago

Is the only thing left to work out how to initialize and update information at the top of the tree?

Also, in a redux like system, most people use bindActionCreators to compose their action creators with dispatch, but that is kind of imperative for the components. Since DOM events are the ultimate sole data source of components, and their return value is currently unused, you could instead have the top-level tree() emit the return values of all DOM event handlers (without bubbling), like so:

E.g.

function render ({actions}) {
  return <div onClick={e => actions.openModal()}>Open</div>
}

// ...

const app = tree(<App/>).pipe(store.dispatch)
store.subscribe(getState => render(app, document.body, {state: getState()})

This would make testing a bit easier, and also make it easy to pipe actions into other sinks, like a global log or whatever.

DylanPiercey commented 8 years ago

@anthonyshort regarding nested contexts; I think they are useful. AFAIK react-router (and other routers) use this kind of context to allow for fancy conveniences like relative routing paths.

It should still be pretty simple even with nested contexts, In one of my libraries I am accomplishing this by providing an explicit "with" function which simply modifies the global context during the invocation of a function and returns the resulting component/nodes. This allows for modifying the context at any level but is also light weight. React originally did something similar to this as well.

Also IMO state helpers inside deku are not needed as libraries like "immstruct" and "baobab" can easily provide this. Not sure if this is the case for everyone, but it seems that whenever I need state that is specific to an individual element/component that often it makes the most sense to simply store that data on the dom directly with some abstraction.

@ashaffer you have mentioned a few times about how critical it is that nodes receive id's. Why is this so important? It would seem to me that if it is an issue with identification of unique nodes to provide state then it would be easy enough to leverage the dom nodes in a map. I am curious how the rest of you are storing your state and how you would handle data that is unique to an individual component?

joshrtay commented 8 years ago

@anthonyshort What's the status of 1.0?

I like what's happening in 1.0. The render functions of components just receive props. I think that's all deku really needs to do. Virtual element can then be in charge of adding context or state.

anthonyshort commented 8 years ago

Well it's about done. But I'm on vacation so I couldn't polish up it and release it :) just need to get some tests passing, but I'm trying to include docs on how to do things like managing state, routing etc.

qur2 commented 8 years ago

I just read the whole thread and it's very interesting. I do not mean to be pushy, however, being very interested as I'm evaluating tools for an upcoming project, I'd like to know if there is anything coming out soon or not.

Thanks for the good work!

joshrtay commented 8 years ago

@anthonyshort - @ashaffer been working on virtex , a small vdom library which lets you process all vdom side effects with a redux like middleware stack.

You may want to check it out. It looks really cool and you could easily build 1.0 on top of it.

ashaffer commented 8 years ago

That would be neat if you wanted to build it on virtex. I did steal all of deku's tests for virtex, which it mostly passes already, so there's that.

It is designed with a redux middleware stack in mind as the effect processing function, but it isn't really coupled to that, it just allows you to orthogonalize unrelated features in a nice way (e.g. components vs dom ops).