Open joaomilho opened 8 years ago
Awesome. I totally agree with every single decision. I especially like the name changes, because it makes it very clear the direction of the data flow and where the history is located in it.
About that I've been noticing that right now there is in the JS community as a whole a debate that hasn't been explicitly formulated yet between whether side effects should be done in a composition with "monads" (with Promise for example) or as a subscription to a global state update. The main difference (and a big one in my opinion) is that with monads you lose the traceability of the side effect in the global state, so both for the purposes of debugging and analytics and immutability, chaining side effects in Promises is weaker than putting everything on the state. But at the same time is considerably less verbose (otherwise you need to model the part of the state that reflects the side effect and add a reducer for it, and then a subscriber).
To recap the two possible ways, in the "Monad/Promise" approach:
const getTranslationAsImage = (word) => new Promise((res) => {
someTranslationService(word, (translation) => res(translation)
}).then((translation) => new Promise((res) => {
getGIFForWord(translation, (gifURL) => res(gifURL)
})
const getTranslationAsImage('vaca').then( ... )
And the state approach:
const reducer = (state, action) => {
switch (action.type) {
case 'translate':
return { ...state, pendingTranslation: action.payload }
case 'translated':
return { ...state, translated: action.payload, pendingTranslation: undefined }
case 'imagefied':
return { ...state, gifURL: action.payload, translated: undefined }
}
}
const outputs = [
lens('pendingTranslation', (pendingTranslation) => someTranslationService(pendingTranslation, (translation) =>
history.push({ type: 'translated', payload: translation })
)),
lens('translated', getGIFForWord(translated, (gifURL) =>
history.push({ type: 'imagefied', payload: gitURL })
))
}
history.push({ type: 'translate', payload: 'vaca' })
Now my point here is that, as I see it, Act supports both, and in a way encourages to hybridize between them, and that's a good thing. I believe there are examples such as the one with the word in which reflecting all the side effect process in the state is just cumbersome and doesn't give much benefit, since the value of restarting the process from the moment the translated word was retrieved is questionable. There are plenty other side effects however that are a semantically relevant application state (the most important that comes to mind is "saving", in which the "in flight state" of attempting to save is useful for the UI to be able to show a spinner), and for those it would be problematic not to have the history option.
I think there is probably room to explain how to handle this more in detail, since for example Monad/Promise like chaining in a history driven architecture can be done in two parts: upstream (before the action is created and pushed to the history) and downstream (as part of refining the side effect). A clear example of upstream is filtering out click inputs so that they only reach the history if the user was also pressing the key "Ctrl". A clear example of downstream is retrieving the translated gif image like in the example above.
This is a little bit of a side track of the change proposal, but I have the impression that with the update that you are proposing Act will approach even more the architecture that I'm describing above, so I wanted to put down some thought about that. Now it's pretty easy to see that side effects done as part of an input are chained upstream and side effects done in an output are chained downstream: maybe those concepts might help with understanding why there are two ways of doing side effects, and when to use each. It also makes it pretty clear that history side effects don't have to be at all related to the DOM.
Cool.
In fact I already have an implementation almost there, and it is beautiful how it simplifies the code, specially in terms of readability.
So after a couple of weeks reflecting about it I'm about to make a major change to the way act works. Most of the ideas behind it come from discussions I had with @xaviervia and some other people doing "heavy" UI work here in Israel. So here are the principles:
main("Hello World")
feeling;Changes
Subscriptions will be called "inputs" and all side effects will be called "outputs";
Example:
What this does is the following:
position
is a subscription (that already exists) that emits an action w/ the current mouse coordinates when the user moves it. This part works the same as it works today. Now,someFunction
, being an output will receive, by default, the history on every change, so it can do anything with it, like simply log the state:This app, as you can see is not about HTML/DOM, and if we changed the sort of input to something that doesn't deal w/ a browser it could be a node app.
Now, since these input functions receive the history on every change, they could be extended to be more handy, e.g. to deal with HTML/DOM:
So
dom
receives a function and returns a function (dom :: (a->b) -> (a->b)
). Instead of passing the history it passes only thehistory.state
to theview
function, and it could even check if it changed (to make it easier the default History should always have the current and the previous state). It also gets the result of callingview
and renders the nodes. This way we could have these sort of functions for different render engines.So far so good. Now let's say we need to change the url when a user clicks a button. Before we would attach an action to a button and perform the side effect right into the action. This is the way most people do it, but most of the time (if not all of the time), they also have to change the state to keep this info somehow (think about the classic
loading
flag). With this proposal it will work the other way around: all user interaction change some state, and state changes trigger side effects (like DOM always worked!). So it would look like this:So
lens
(or some better name) gets a path (as in Ramda's lens paths) and a fn, and it "watches" the history for changes on this path, that is, ifurl
changes. When it changes it will callnavigate
and perform the side effect.Of course, many side effects could be even more specific, like subscriptions are nowadays, so we could have things like
route
,fetch
, etc...On KIS
At last but not the list, to keep the feeling of simplicity, outputs could be a list or an item. If it's an item act would suppose it's about HTML. So if one does
main(dom(view))
, or evenmain(view)
we could automatically wrap it into a dom handler. Therefore,main("Hello world")
would still work.