30 ms·
A simpler web architecture using React, Flux, CSP, and FRP concepts
- staltz 11y agoThere is absolutely nothing related to FRP in this blog post, not even "composable event streams" for that matter. This is just using a CSP channel as a (global) event bus. It essentially replaces the (global) dispatcher with a CSP channel. This blog post's approach is essentially typical Flux and nothing else. And doesn't improve to make it more functional either, because both `onClick={this.clickHandler}` and `yield csp.put(channel, {actionType: 'incr'});` are imperative code.
- mst 11y agoPlus both redux and fluctuations work using immutable state and reducers rather than mutating actions.
- tzaman 11y agoThe problem is that generators are not that widely supported (yet). You could replace CSP with something like Bacon.js and likely achieve the same effect (don't have an example, but I'm researching the area for a new project)
- xtrumanx 11y agoAren't generators part of Babel? As long as you target ES5 shouldn't you be able to use generators via Babel?
- kenOfYugen 11y agoGenerator functions are part of ES6/2015 and are supported by babel. But keep note that downgrading them to ES5 spec. includes a performance trade-off.
- jevakallio 11y agoTake a look at http://cycle.js.org http://cycle.js.org - it implements essentially the same concept using RxJS instead of csp and virtual-dom instead of React.
- stardrive 11y agoExcept remember, RxJS is also a growing standard and the virtual-DOM Cycle.js uses has been tested to be a fair amount faster and robust than the one React has. Then there is also Cycle's Model-View-Intent pattern and leading edge work on component integration, which is in line with W3C standards. Cycle's drivers also allow Cycle.js to integrate or target many options. Things have been fragmented and Cycle.js seems to have brought the best together, representing a new and intelligent vortex of progress. Before I took the time to understand what Cycle.js was about I was naive and thought React was it. I now understand how poorly it's reactivity has been implemented. On the other hand Cycle.js is fully reactive and has brought structure, flow and a sense of intelligence back to my app development.
- jarpineh 11y agoFor a light weight, ES5 style version, I have used a function generator (or factory, if you prefer). It keeps iteration state in its closure, and the resulting function updates it when its called. Your code calls generated function from then on. You can easily use arguments to reset the state, or to get something else to happen. function gen_f() { var count = 0; function f() { count += 1; return count; } return f; } Probably you could use csp.take(channel) like in OP this way, too. I used this solution primarily because I couldn't figure out how generators handle arguments in iteration calls. My problem was to find a string that should be returned for a given index (where there was some complex rules on what string was active when) and to keep looping a list indefinitely. Generators seem to make things syntactically cleaner, but for me at least reasoning about them has been challenging in JS context.
- kenOfYugen 11y agoThey are widely supported: https://facebook.github.io/regenerator/ https://facebook.github.io/regenerator/ Yes, you can replace CSP with Rx style libraries. It's just a different kind of learning curve. Personally I prefer CSP, as channels are a more primitive approach to writing concurrent applications. CSP is like a library and Bacon/Rx are like frameworks.
- jarpineh 11y agoThis looks really fun way to develop React apps. I just developed a React app, and managed to trip over state related problems at every stage. First with full state outside components, then within a top component, then I tried with a simple store type concept. Whenever I had to deal with async issues, I made a mistake with communicating ongoing state or getting React to notice changed state. I was waiting for Redux to go 1.0 (which it apparently did yesterday!). I wonder how async updates, like network requests, to the model should work? Perhaps two channels, one for events from the components to what ever is handling requests for you (I just used D3, since I have some other uses for it), and then it can message the results via channel to what actually updates the model. OP's example reminded me strongly of Mithril [1]. Though with addition of CSP channels to communicate changes, and explicit external render call, which Mithril allows also, but I don't remember having to use. It's clean and simple looking, and I like it. Now I got one more way to mess with my app state. And should I decide to use Mithril again on other projects, CSP seems like a worthy addition if it can be applied this easy. [1] https://lhorie.github.io/mithril/ https://lhorie.github.io/mithril/
- davexunit 11y agoI have a small project that I poke at that uses Mithril + Kefir for a nice virtual DOM + FRP architecture. A few lines of glue code and I was good to go: https://git.dthompson.us/guix-web.git/blob/HEAD:/js/utils.js#l48 https://git.dthompson.us/guix-web.git/blob/HEAD:/js/utils.js... (pardon the self-signed cert)
- jarpineh 11y agoKefir looks interesting, thanks for the pointer! Now if somebody makes async channel combinator so that I wouldn't had to build so much boilerplate for network requests. Be it direct callbacks or promises or the new fetch specification, control flow of async operations seems to require more than one channel and glue code in between. That seems to bend the paradigm a bit too much for what is essentially IO ops in my apps.
- fleshshelf 11y agoIt probably won't scale well due to the number of ddp connections required, but I'm using Meteor and React for a side project. It's very pleasant to work with.
- pavlov 11y agoWhy not have "incr" be a method on the model? Then the update() function with the big switch on "actionType" could be replaced with: model[action]()
- Retozi 11y agoI think this is a bad idea. First with methods, you have mutable state, which I believe should be avoided. It causes a lot of problems for efficient rendering. You could make some kind of wrapper, but I guess it's not worth the hassle to save a couple of characters. Second, Action can cause complex writes that touch more than one model (or if you have one global model, multiple state slices). You probably want to have the write functions named independently, so they can be reused over multiple actions if necessary.
- pavlov 11y agoYou don't avoid mutable state by doing "model.count += 1" in a separate update() function rather than a method on the model. Both are functions that operate on the same data; the OO syntax is just a convenience that was invented exactly for the reason of avoiding endless switch statements in dispatching actions on a particular piece of data.
- danielmason 11y agoDoesn't the OO approach just move the work of the switch statement into the class declaration of the model? In practice, OO is about encapsulation, and FP is not. So it's probably not too surprising that the author chose not mix metaphors. This is also modeled after an architecture in a statically typed language that expresses possible actions as a single union type, in which every possible type must be handled. There's no similar compile-time guarantee in JS, but a switch statements with constants is a reasonable approximation of the idea.
- jergason 11y agoThis is exactly the Same as Elm, without the type system to help you out.
- sbensu 11y agoYeah, and without Signals to make things like mouse position transparent. I started using CSP in Cljs + React applications, but I find signals to be a better abstraction for UIs.
- erokar 11y agoAgree. ReactJS was inspired by Elm as far as I know, Purescript has a framework purescript-signal that is inspired by Elm, ClojureScript has Zelkova that is inspired by Elm, and on it goes. Makes it more and more tempting to just go straigth to the source and simply use Elm.
- insin 11y ago> Makes it more and more tempting to just go straigth to the source and simply use Elm. Redux was inspired by Elm's update concept, not React. I was on that train of thought too after using Redux had the side effect of giving me a conceptual understanding of how the update step works in Elm, but I found that "simply" really isn't the right word if your experience is primarily with imperative languages. e.g. building an app starting from HTML5 history and routing built on top of it is just another API if you already know JavaScript, but it's an entire new set of concepts to learn if you're not au fait with functional programming, as I quickly discovered!
- seanmcdirmid 11y agoI think React predates Elm, and FRP has been around since 1995. React also isn't very FRP, it has more of the feeling of an immediate-mode GUI.
- tlrobinson 11y ago(FYI Wikipedia says Elm was released in 2012 and React in 2013, but that doesn't mean Facebook/Instagram weren't working on it before. Also Asana has been doing similar things since ~2009.
- tel 11y agoThe two challenges past this point are (a) making the components ignorant to the larger structure of the single state enabling things like component-local state and state representation independence and (b) getting creative with how to construct reducers composably so that various domain concerns are separated from one another in the construction of the core state.
- geonic 11y agoHow do you make sure that all your components have access to the same channel? In a typical setup you'd use something like webpack to import modules from separate files. Calling var channel = csp.chan() in each of these will create unique channels, doesn't it? The way I see it you'd need to pass this channel down to each component.
- tlrobinson 11y agoI don't know js-csp (I prefer ES7 async/await + promises or streams) so correct me if I'm wrong, but the "csp.go" here isn't required since it's a single operation and the return value isn't used: csp.go(function*(){ yield csp.put(channel, {actionType: 'incr'}); }); I assume it's essentially the same as doing the following using async/await: (async function() { await csp.put(...); })(); Which is pointless.
- idibidiart 11y agoI think the idea is to update the model asynchronously via put before rendering the new state via take. I'm not entirely sure (I don't know csp much) but I think using a single CSP channel makes that trivial, so you're always updating before rendering. Would appreciate more insight and/or correction...
- tlrobinson 11y agoRight, but I presume csp.put() is asynchronous already, there's no point in wrapping it in csp.go().
- slake 11y agoHow does this scale to multiple views, models, etc?