10 ms·
Om Sweet Om: functional front-end engineering with ClojureScript and React
- jlehman 12y agoI've had the same experience building http://fitsmeapp.com http://fitsmeapp.com with Om/Clojure -- great to see real, production apps being built in CLJS!
- presty 12y agoyour scrolling seems to be broken when using chromium and arrows (up/down) when going all the way down and then up, it gets stuck around the 2nd or 3rd panel
- jlehman 12y agoThanks for letting me know -- I'll look into it.
- acconrad 12y agoI think your sprite for your logo is all messed up (looks way too large). Also have you heard of http://www.eatthismuch.com/ http://www.eatthismuch.com/ ? How does yours differ?
- jlehman 12y agoJust learned about that recently -- it's a retina display issue (fix going up tonight). Thanks for pointing it out! I have heard of it and used it myself. The primary difference is really the intended use case. Where ETM is focusing on highly-specific (usually on a nutrient/caloric basis) meal plans to hit a target nutritional intake, we are more centered around using general preferences about food (ingredients, cuisine, cooking methods, etc.) in a "browsing" capacity -- currently in the form of recipes aggregated from other sites and bloggers. The goal is eventually to also incorporate nearby restaurant dishes.
- dustingetz 12y agofwiw it's possible to achieve all of these benefits in vanilla react using javacsript with regular javascript data structures * single mutable ref to normalized app state * cursors for encapsulation and modularity * O(1) deep equality checks * fastest possible react performance source: I have done it in two large react apps, but i have not documented it yet. Blog post coming soon. Interestingly, as your app state gets bigger, you are actually forced to use something like cursors for all state updates, for performance reasons. Naive immutability in javascript means clone-before-modify, which is far too slow when your state values are large. It quickly bubbled up in our profiler. Funneling all immutable updates through a cursor means the cursor can use structure sharing on the state value, skipping the defensive clone and preserving reference equality for equal subtrees. Also afaik, implementing a general shouldComponentUpdate (edit: in js) without something like cursors is not possible, because you need === equality for onChange props.
- beat 12y agoIf ClojureScript had (or has) something like Clojure refs, controlled high performance mutability should be possible.
- swannodette 12y agorefs are not the highest performance reference type due to transactional semantics. ClojureScript already has efficient mutability in the form of atoms and transients. You can actually build a transient vector faster than you can build a mutable JavaScript array in recent builds of V8. I actually demo'ed this at JSConf. Fun stuff.
- dimsuz 12y agoWhich JSConf was it? US 2014? Tried to find a video of this talk of yours, no luck so far.
- ericflo 12y agoIt's funny, I ran into the same problem a few months back and came up with something similar to cursors as well.
- skrebbel 12y agoThanks for this post! I'm very inexperienced with Clojure, but I'm a big fan of React. I've always wanted to understand how Om encourages immutability and how it deals with "UIs are stateful by definition", but I never managed to wrap my head around it, probably due to my missing Clojure skills. Your post explained it in general terms. I will probably apply the same in plain JS now. It sounds like an interesting challenge to implement the Cursors idea using React's Update addon [1] instead of depending on Mori. (I don't like Mori because I don't get the feeling that it's intended to be used without ClojureScript - its API is rather cumbersome from plain JS) [1] http://facebook.github.io/react/docs/update.html http://facebook.github.io/react/docs/update.html
- swannodette 12y agoI'm curious about the "cumbersome" claim. Is it because Mori forces you to write in a functional style? I've tried as much as possible to supply an API that doesn't stray too far from conventions JavaScript programmers are used to from underscore, lo-dash, etc.
- skrebbel 12y agoOh, it might have very well been me being to much of a newbie to even understand what was going on, so please don't take the claim too seriously. I believe that it had something to do with it being relatively much work to update things deep in a tree. Like, if I have Model -> Users -> User -> WallUpdates -> WallUpdate -> Comment, and I want to make an update to a comment, I'd have to do a lot of code writing. Is that true, or did I just miss the magic function that helps me do this? But I'm now realizing that maybe, if I want to "manually" do that, I'm just doing it wrong - this should partly be what Om's cursors are meant to avoid, right? (this, and locality to components) In general, if it helps you as feedback, I found Mori a little difficult to approach, having no Clojure experience. I suspect that some more examples of typical usage patterns at the top of the docs would do a lot. Like, which methods are supposed to be commonplace, and which ones are more for edge cases? What kind of things am I supposed to do all the time, what kind of things should I wish to avoid except in these and these cases? FWIW, I feel like the same holds for Underscore/Lo-Dash's docs, except it's been less of a problem for me because I'm more familiar with typical usage patterns of mutable data structures.
- andrejewski 12y agoFor people who have wanted to try this type of programming out but have not jumped the boat from JavaScript to ClojureScript, I created a little library that mixes Backbone and React in a similar model described in the article: https://github.com/andrejewski/reactbone https://github.com/andrejewski/reactbone It takes away a lot of the data-binding cruft that you find in Backbone.View and works well with React without making you switch languages, design paradigms, or add new libraries. The Backbone component makes it easier to build data hierarchies than with plain JS, but I assume other MVC frameworks could be retrofitted to work with React.
- cnp 12y agoExamples would be lovely
- Touche 12y agoIt's going to be much nicer when ClojureScript runs in Node without needing a JVM. Now a ClojureScript project requires a lot of boilerplate and running cljsbuild auto. For big projects this is fine but I want to write smaller scripts in ClojureScript as well. Basically I want to do: cljsbuild app.cljs > app.js And that's all. No project.clj, no waiting 5 seconds for clojure/jvm to load and Closure to compile it all.
- lennel 12y agoclojurescript run in node? is this on the roadmap?
- dkersten 12y agoClojureScript already runs on node: https://github.com/clojure/clojurescript/wiki/Quick-Start#running-clojurescript-on-nodejs https://github.com/clojure/clojurescript/wiki/Quick-Start#ru... I think what GP is complaining about is the compilation step, which requires a JVM and isn't exactly the fastest. Touche - You don't need project.clj and leiningen and can run a command basically like you're asking for. I'm not sure how much faster it is than using leiningen and cljsbuild, as I haven't tried it: https://github.com/clojure/clojurescript/wiki/Quick-Start#clojurescript-compiler https://github.com/clojure/clojurescript/wiki/Quick-Start#cl...
- lennel 12y agothis is what I was reffering too, the compilation step. It seems very daft for me to loose the closure compiler. I assume lein keeps the jvm hot, if not writing a script to do it should be pretty simple.
- dkersten 12y ago`cljsbuild auto` keeps the jvm hot and rebuilds any time the source changes. You could also use something like Nailgun too, if that is more desirable. When I develop in ClojureScript, I generally avoid rebuilding altogether whenever possible (auto or otherwise) and instead have my editor send changes to the browser REPL. I don't know if you can do this with node too, but it would seem odd if not. This seems to be the preferred approach by most Clojure devs.
- w01fe 12y agoEngineer @ Prismatic here. I'm happy to answer any questions about the post or our experiences with the rewrite.
- dkersten 12y agoAn Om question: what do you store in the app state and what do you store in component local state? In my own application, I started off storing everything in app state, using local state only for short-lived transient data, but over time I found that having a bunch of information like what tab is currently visible in the app state became difficult to manage and made the app state a nightmare. So now I like to keep a strict separation: app state is basically my model - the data being processed (if I were writing a text editor, its the document being edited) and local state is everything else: which dialog is open, which tab is selected, button toggle state, mouse state etc (in a text editor, whether bold is selected, whether I'm in print preview mode or edit mode, etc). Basically app state is what is being processed and local state is how to process or display it. This separation also means that the app state is what gets synced with the server. I like to think of it as "if its something I might want to persist in the database, then it lives in app state, if its something I would never want in the database, then it lives in local state" I'm curious what other peoples experience with this is. Another Om question: how do you sync state between client and server, or how do you handle communication? I looked at how om-sync does this (using tx-listen to automatically sync changes), but ended up building a more traditional request/response model using sente[0] and having my app ask for sub-trees of my app state as needed. Again, just wondering about peoples experience with this and different approaches tried. [0] https://github.com/ptaoussanis/sente https://github.com/ptaoussanis/sente
- cnp 12y ago#1 is a great question that I've been curious about as well, particularly for larger UI's
- w01fe 12y agoWe're still figuring out exactly where the boundary lies, but our current approach is much like yours -- view state that's purely local to a component goes in local state, and everything else goes in app state. This feels more natural, but means you lose some of the nice benefits of things like replay/undo for this local state. For client/server, we've started building up some nice abstractions around our API (like fetching more data in a generic paginated list stored in a cursor), but the write side is still mostly manual. We're trying to use the same API for iOS and web, which probably prevents us from doing some fancier things that continue the same architecture back into the server.
- presty 12y agoI'm interested in knowing more about how people are architecturing their Om projects e.g. are you using something like: app component <-> page components <-> (widget) components where the app creates the page and the page creates the widgets and communication is flowing through channels? and how do you deal with url transitions? do you capture and flow up the click from a widget back to the app so it knows it's time to render a different page? and have some (case ...) at the app level? or using some kind of mix with secretary and changing the location.href/a hrefs? etc
- w01fe 12y agoYep, that sounds about right. For us, API communication and some global events flow through channels into the app state, where it's distributed to components. For routing we've built up a library around secretary, where we write down a map of uris, handlers, and metadata (can be a modal, etc). Info about the current page and such goes into the app state and that drives the transitions between page components.
- haberman 12y agoOne thing that's unclear to me: does this require the entire application state to be loaded into memory all the time? If your application state is large but you only care about a few parts of it at a time (a few "cursors") how does Om deal with that?
- swannodette 12y agoNot sure what you mean? Om uses whatever state information has been delivered to the client browser same as other JavaScript approaches/frameworks.
- haberman 12y agoI saw some stuff about cursors as a way of displaying/modifying only a part of the global state. But it wasn't clear to me whether this is a formalized way of only loading part of the global state, or if cursors still require that the entire underlying "global state" is loaded into memory. -- To give more background, let me give more specifics over what I'm thinking about when I ask this. I've been working on a JS accounting app. One thing about accounting is that the balance of an account is dependent on every prior transaction that the account has ever had. So modeled in functional terms, it seems like you'd have to make this a tree of depth "N" where N is the total number of transactions in the account. It seems like you have to model it this way to get the data dependencies right (a change in an early transaction affects every subsequent transaction). A functional approach with such a deep tree seems to break down (at least from the relatively little I know about Om). For one, I don't want to have to load every transaction into memory. Even though the most recent transaction depends logically on the first one, I don't want to be forced to load the first one just to show the most recent one. One possible way to work around this is to collapse all transactions prior to the ones I want to display into a single "fake" transaction that stores the accumulated balance as of this point. But now my in-memory model has to differ from my real model, and as the two diverge things start to get more complicated. A bigger problem: I can't think of a good way, with this design, to indicate to the data layer which transactions the UI actually cares about and wants to have loaded into memory. If the tree has only one logical root (the most recent transaction) and is logically N transactions deep, how does the UI indicate which of those N it actually cares about? Maybe Om cursors can do this somehow? I've been using a more traditional mutation-based/observable design, which seems to be working well. The UI can "subscribe" to certain transactions/balances, and this serves as a signal to the data layer about what needs to be loaded and what derived values need to be kept up to date. The data layer can be smart about how many transactions it actually needs to load to compute what the UI cares about.
- kylebrown 12y agoI'm real curious about the potential interplay between core.async and om/react. Bruce Hauman has a great tutorial on using core.async channels (as opposed to event callbacks) to process user input and gestures.[1] David Nolen (creator of Om) also covered core.async.[2] But these tutorials pre-date Om; core.async came to clojurescript in summer 2013, Om wasn't released until 2014. I guess the way React handles input is by attaching event handlers to child components, so that changes to the child properties bubble up to the root component state. Is this the optimal pattern for handling events in a React app? Or is there an advantage to be gained by using core.async channels to process event input for Om/react? I haven't seen any examples which use both core.async and Om/react, but I'm very curious about the possibility. 1. http://rigsomelight.com/2013/07/18/clojurescript-core-async-todos.html http://rigsomelight.com/2013/07/18/clojurescript-core-async-... 2. http://swannodette.github.io/2013/07/12/communicating-sequential-processes/ http://swannodette.github.io/2013/07/12/communicating-sequen...
- diego 12y agoCheck out https://github.com/sgrove/omchaya https://github.com/sgrove/omchaya
- presty 12y ago> I haven't seen any examples which use both core.async and Om/react, but I'm very curious about the possibility. that's strange, because Om's tutorials use core.async https://github.com/swannodette/om/wiki/Basic-Tutorial https://github.com/swannodette/om/wiki/Basic-Tutorial https://github.com/swannodette/om/wiki/Intermediate-Tutorial https://github.com/swannodette/om/wiki/Intermediate-Tutorial
- kylebrown 12y agoAh yup, there it is in the basic tutorial, "Intercomponent communication". It is strange that I don't remember noticing that when I skimmed it. I must've been too distracted counting parentheses and trying to wrap my head around clojure syntax. I'm relieved to know it was there the whole time and I simply missed it, thank you.
- 12y ago
- deleted 12y ago[deleted]