Closed Raynos closed 10 years ago
Good questions.
Regarding the Hideable
: Yes, if using only Quiescent components, Quiescent would require that the fact that the component is hidden or not be present in the value used to render the component. Note that it doesn't have to be simplistic as a {:hidden true}
value, however; it is perfectly legitimate to have the renderer invoke view-specific code that decides whether an item is hidden or not based the value provided. For example, the TodoMVC example does exactly this with its filtering mechanism; the view state only specifies what filter is present, and the rendering code compares that with each item to see if it should be hidden or visible.
The same goes for a SortableTable
; the current sort column and direction would need to be specified somehow in the value used to render. Note that in many cases, this is a good thing; it means it is possible to share and persist UI states across sessions (if you want).
The tradeoff is, of course, that it does make it more difficult to write a fully encapsulated UI widget that provides stateful functionality while remaining agnostic to its location. If you rely heavily on such components, Quiescent might not be the library for you. I can, however, think of several approaches:
Your final point is correct. In the Quiescent model, for a component to change anything, it (or, more precisely, a handler function that it defines) must somehow reach back out and update the higher-level state and cause a re-render. Exactly how that works is up to you; Quiescent specifically does not impose any particular technique there. You could pass in a cursor, a-la Om, or core.async channels, like I do in the TodoMVC application. (You could also swap an atom directly in the handler function. But ew, gross.)
My recommendation is that you think of your application's handler functions not as getters and setters for your applications state, but as inputs for your system:
@levand this means that you would need to do something like
render(data, channels)
and pass the data to render and a channel into a SortableTableComponent.
The problem now comes that the SortableTableComponent
actually owns a set of functions that loop over the channels and mutate the correct state.
i.e. the channels that get triggered when a user clicks the headers should mutate or change some meta data about sort orientation. The user shouldn't have to re-implement that code.
If a component could encapsulate it's own state, it could choose to manage atoms & channels or manage om style cursors. It would mean the component is portable, i.e. the code that reads from channels and mutates state is not hard coupled to the high level decision of how the application represents its model.
At best the user of a re-usable component has to buy into the channels + use my documented data structure decision for the entire application that the component author made and would have to use two sets of functions, one in it's rendering logic and one it's channel reading logic. This seems very tedious.
Well, you could create less coupled abstraction point by providing a render function of the form (render data get-state set-state)
where get-state
and set-state
are functions over the data that may be freely invoked by the component to store and retrieve its "internal" state, and are implemented by the application in whatever way makes sense for its particular model.
That would require some additional work for users of the component; however, even that could probably be worthwhile. Especially, if all stateful components used the same idiom, they could even be passed the same functions.
Or, as I said, the right thing might be to fall back to a non-Quiescent component if you really want transient local state.
Regarding your final point about using two sets of functions - I'm not sure I understand how that's different from any other system. Logically you had to do the same work anyway (rendering and updating), Quiescent just encourages you to handle those as distinctly different concerns. And, its true that using core.async channels does require you to implement both ends (producer and consumer), but that's inherent in its value proposition (coordinating decoupled processes). If you don't want that you probably just shouldn't use channels.
I realized the problem with transient local state is that it breaks referential transparency of higher order pure components.
You can't embed one of these stateful components as a child of a referentially transparent component because you wont re-render it unless it's state has changed.
Even with (render data get-state set-state)
you can't apply the optimization of saying the first argument hasn't changed so lets skip rendering, you need to know whether the transient state has changed.
React.js gets around this by doing subtree re-rendering, i.e. it an re-render a tree from any point, not just top down.
One truly needs to make a trade off between the modularity of transient state and the referential transparency of stateless render
.
I am working on an example that I think will address your concerns.
@levand I'm more and more convinced that transient state is a terrible idea. and that I should simply build components around render(state, channels)
One thing I'm really interested is benchmarks of React v om v quiescent v reagent
Well, I think there is a legitimate tradeoff there. The point you make about encapsulation is real; it definitely is more work to design a good, generic, reusable component when rendering using only top-down data. Personally, though, I think that's still the right way to go.
I've been thinking about some ideas for benchmarks; will definitely publish some when I can.
@levand excited waiting the example you're making.
You redefined the word less with this wrapper. Sincerely hope it flourish.
Regards,
Geraldo
@levand Could you explain why do you always put values on components on todomvc with (go (>! )) instead of (put!) ? Considering you are not in a go block context when rendering and most operations are not async your main go block would not have the risk of dying if you had used put!. Is that correct ?
@levand did you ever make an example for this ?
I haven't had time to make a good (public) example yet, though I'm using Quiescent pretty intensively for a client project so it does have attention being devoted to it.
I have some ideas for more open-source examples, but am not sure when I will have time to publish them.
I'm closing this issue for now as it doesn't have anything immediately or specifically actionable - watch this repo and my twitter feed for when I do get a chance to put examples out there.
@levand
I actually started authoring a react-style framework that bans local state. I don't think there is a need for it and will just not use it.
The pattern I've been using is
var state = mercury.hash({
sortTable: SortableTable(...).state
})
function render(state) {
return h('div', [
RenderSortableTable(state.sortTable, ...)
])
}
i.e. you embed a module's state in your global state atom and then thread it's state to it in the rendering call.
om
,reagent
andReact
would recommend and allow you to implement a stateful component that can close over internal render only state.For example a
Hidable
component might implement or emulate a HTML5<details>
element and close over avisible
boolean which it manages internally without forcing you to place said boolean on your top level data structure.For example a
SortableTable
component might implement the notion of sorting a table based on a column and close over acurrentSortingColumn
index variable and acurrentSortDirection
boolean which it manages internally without forcing you to place those pieces of data on your top level data structure.More importantly, even if those pieces of state were placed on the main application state, how would the components mutate or change the main application state idiomatically, without the components being tied to your state / event system, i.e.
om
cursors orcore.async
channels.