Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Straightening our Backbone: A lesson in event-driven UI development (mixpanel.com)
105 points by tdumitrescu on April 8, 2015 | hide | past | favorite | 27 comments


This is a great example of "simple is better than easy". It always seems so easy to "just make a callback" in JavaScript/UIs but when things go beyond a very simple UI component things go downhill, FAST. This sort of callback dependency nightmare is so common in workflows adopted by many JavaScript/UI frameworks. It's scary and sad at the same time.

I encourage any JavaScript developer to seriously take a look at ClojureScript + React + core.async for front end development. Not a silver bullet, and there's the learning curve of a new language, but definitely better tools to build the foundation for an asynchronous & inherently complex UI. If the problem/project you're solving/working on is not trivial, the payoffs are there.


> It always seems so easy to "just make a callback"

DOM interactions are callback oriented. That's how the DOM is built. That's why people write callbacks at first place. Why would you want people to work differently? the api is what it is. You might think the api is broken, well, it's not the front end developers fault, they didn't write the spec.

> but definitely better tools to build the foundation for an asynchronous & inherently complex UI.

React and CO are DOM abstractions. Which is fine if you know how the DOM works at first place. The problem is most people don't bother learning how the damn thing really works.

But at the end of the end of day, no front end developer can escape from JavaScript or the DOM. When things break,you still have a big JavaScript stack trace with DOM errors to debug,language A or B,framework X,Y,Z or not.


I agree with you that there's no escaping the fundamental way the DOM/JavaScript was setup in the first place - it's too late unfortunately, that ship sailed in the 90s apparently :). I can write at least an article about this stuff, but if you're interested in how core.async specifically helps at least mitigate some of the callback spaghetti hell here's a good starting video https://www.youtube.com/watch?v=AhxcGGeh5ho


word.


Terrific write up. Recently I've been using the mantra "design transcends (choice of) technology" and nowhere is it more apparent than with front-end frameworks. Using React or whatever is hot will not make much of a difference at scale if the application is not well designed. And conversely, it is possible to design great apps with all sorts of alternatives to the brand du jour. I'm still a big fan of the shiny new stuff, but kudos for applying new and improved design concepts to your existing stack rather than jumping brands under some delusion that it will solve all your problems. The community needs more thinking like this.


Definitely. I was surprised at how thoughtful and sane this article is.


So, one flaw in the approach they displayed is the mediator is kicking off two asynchronous processes. Yes, naturally race conditions will occur if process A competes with process B. Furthermore, why is a mediator performing business logic? The dispatcher / mediator / router / orchestrator seems to be presented as an active object in of itself. It really shouldn't be.

Rather than manipulating or calling components from your mediator, you could tell a coordinator that coordinates services. Services are then where you're housing your business logic. This allows you to structure these service calls and account for execution order.

>View initialization and render are separate steps

yes

> no events are fired during the app’s initial bootstrap/render process

sure

> on a given UI screen, a single top-level mediator takes on all responsibilities of communication/event-dispatching between subviews

> In practice, the code of the Orchestrator view becomes the centralized location and source of truth for all inter-widget communication in its purview

no?

There's a reason why actor systems are a thing. You can't just stuff everything into a central object like it's a flat key/value store. It's better (in my opinion) to form a hierarchy of responsibility. The "Orchestrator" should hire some subordinates and delegate work to them in an effective manner.

You guys must have an insane amount of business logic in your web app for any given UI screen (which as I'm understanding it is different menu options I guess? because otherwise a single UI screen is your entire app). The main takeaway in what I'm saying is a router shouldn't be confused with a component that executes business logic. It's doing too much.


> So, one flaw in the approach they displayed is the mediator is kicking off two asynchronous processes

hmm, seems like the diagram wasn't very clear on that point. there's only one asynchronous action there: making a network request back to the server for fresh data, which by its nature has to be an async op. everything else is synchronous (including Backbone's event handlers).

> The "Orchestrator" should hire some subordinates and delegate work to them in an effective manner.

this is precisely what's happening: the orchestrator basically does nothing but delegate between subviews and the model layer, ensuring that messages are passed around as appropriate. the text probably didn't make clear enough that this is supposed to happen at any sufficiently complex portion of the UI ("This pattern is repeatable: any sufficiently large or complex area of an app (such as a single route) may hold its own mediator to encapsulate the details of its UI interactions and communicate with the Model layer with a small, well-defined API"). we don't really consider interactions between UI components to be "business logic;" if the 'mediator' started turning into an uber-object with many responsibilities we would probably look at breaking it into services (or putting the right bits back into the model layer), but we try pretty rigorously to avoid premature abstractions...


ah, very interesting explanation thank you


This is a very good breakdown of a useful pattern. I think they dismiss declarative approaches with too broad of strokes, but there is always room for frank discussion of lighter, straightforward approaches that get the job done. This overlaps with Codecademy's messaging architecture with React presented by Bonnie Eisenman at React Conf [1], except Codecademy took it a step further and dynamically generated the mediator linkage using mixins pertaining to the views in play for a given screen.

We pretty much write desktop apps in this style but run into problems in some complex situations, so I am parsing this for clues to where we may be letting problems sneak in. The rule that programmatic UI updates never raise events may be such a clue, indicating some views are doing too much direct interaction with both local state and subviews.

Question: are there rules to apply to this architecture to help with data modularity up and across the view tree? A lot of the work around React right now has to do with creating techniques to avoid passing state across levels that don't directly depend on it. For example, communicating an event two levels up without involving the intervening parent or communicating across subviews that don't share a common parent. As I read the OP, you have to pass the event all the way up and the state all the way down, coupling the dataflow and triggering rerenders pretty much all along the path.

[1] https://www.youtube.com/watch?v=ZM6wXoFTY3o&feature=youtu.be


"Our Unix-inspired development philosophy favors the integration of lightweight, independent apps and components instead of the monolithic mega-app approach still common in web development."

I have essentially ported Unix itself onto the web at https://www.urdesk.net . You can say that the site itself is a kind of "mega-app", but given that it is an entire OS-in-a-browser that typically "installs" in < 2 seconds, I think it can be forgiven. There is also an app that is a kind of "Hello World" for AI that uses the Speech Recognition and Voice Synth capabilities of the Chrome browser. To hop directly into the AI, you can check out https://www.urdesk.net/desk?intro=bertie

The site is now Chrome-only, since I am pretty much going all-in with the whole AI theme, and none of the other vendors include the necessary Javascript API's.


I deleted root and it froze. Where can I file a bug report? This is unacceptable.


Do you mean: I typed: 'rm /' and the prompt never returned? This didn't actually delete anything, you can always just do a ^C to get the prompt back when it gets stuck like that. I wouldn't consider that a "bug", just an unhandled error condition. More of a "glitch" that will take about 2 seconds to work out. Thanks for the feedback!

EDIT: Already fixed with a nice friendly error message!


Hey friend, I was just joking about "This is unacceptable." Sarcasm :) I do `rm -r -- /`. Very cool project.


> have essentially ported Unix itself onto the web

If by “web” you mean “an up-to-date version of Chrome”, which was what the site told me was the only thing it worked with.


How is this Unix? There's nothing in /bin. Did someone crowbar your install already? Where's find/grep/vim? I'll need those if you want this to be a real Unix. Also, maybe fsn[1] is a requirement ... ;)

[1] - http://en.wikipedia.org/wiki/Fsn


Great write-up.

As far as backbone goes, we've had success using marionette.js to standardize the way we write front-end components and implement common types of view patterns. Curious to see if anyone else has found it to be useful.


We've had the same experience. It adds just enough convention on top of Backbone to prevent your application from turning into a ball of mud, while at the same time remaining small enough where you can hold the whole framework (Backbone + Marionette) in your head. I find Backbone + Marionette much simpler, and much easier to reason about than Angular, but keeping it this way still requires developer discipline. Avoid hacks, avoid event soup (which Marionette maybe makes a bit too easy), and follow general object oriented design patterns, and it's not that hard to build a maintainable, scalable Backbone/Marionette app.


Same thing here. We're a pretty large agency, and it powers all of our SPA's and hybrid apps; even with a normal request based approach, Backbone/Marionette handles all UI beyond routing and initial render.

It's a definite boon to our development proces, and it has made our solutions easier to maintain. Can't ever see us go back from this approach.


We've had great success with Marionette, specifically by leveraging concepts from the http://backbonerails.com architecture.


We are also using backbone + marionette combo successfully and still very happy to do so.


Good article but one thing to be careful of here is that if you just apply this pattern everywhere then you can end up in a situation where every view is decoupled from every other view and there are way more events in the system than there need to be. This happens more frequently than you would think. In the example given of a date picker this is fine as you can easily see that you might want to re-use the date picker somewhere else. But there are often circumstances where a subview could never be re-used outside of it's parent view. For example suppose I have a main calendar view with subviews showing appointments. The subviews need to inform the main view when the user changes an appointment time so that the main view can re-layout all the subviews. In this case there is no need for the main view to listen for events from subviews. The code will be much simpler and easier to debug if the subview just makes a call to the main view directly (the subview should be constructed with a pointer to the main view). When I look at code in the subview I don't have to guess who may be listening to the event, I can see the call directly. It also means that other views apart from the main view cannot randomly listen sub-view events as a 'quick fix' for some bug.

The point is that javascript tends to lead one down an event driven route. But you should only use events if you want to decouple things. Cohesion is also very important in design because it makes things simpler and more encapsulated. You need to decide what the boundaries of your component are. In this case I want the calendar to be the component, not it's subviews. So within the calendar I will make direct function calls, there are less events in the system and it is easier to think about and debug.


But suppose you want to re-use the sub-view in another component, you would then need to clone the functionality and satisfy/remove all the directly coupled invocations.

This is where you have to decide how much coupling to introduce to balance comprehension vs future needs of the project


indeed, it's a matter of juggling competing maintainability considerations and there are cases where it can make sense to couple components tightly.

that said, in practice our UIs contain relatively few one-off components, and it often requires less dev friction simply to use the standard event pattern than to weigh borderline cases to shave off a little indirection at the cost of tighter coupling. the problem of "too many events in the system" is avoided by letting complicated components handle events from their own subviews and not just blindly bounce everything down to a global mediator. e.g. if no other views have to know about the 4 dropdowns and slider within MyWidget (and they usually don't), then MyWidget can handle all those subview events itself and simply present a single unified 'i've been updated' event for other consumers. essentially narrow the public apis between different actors in the overall system.


Very often articles of this nature end up being either too abstract and too vague or too detailed and I would lose focus midway. This is very finely written.


Well written compendium of common design patterns.

I love Backbone for its simplicity but I think most problems come from the use of the views' render function, which encourages the habit to have a _centralized_ way to refresh theirs content.

This is, again, a simple way to think about the process, but often it is not the most performant (and this is what has driven to the clever virtual DOM diff-ing of the recent frameworks).

I normally use what I call 'micro-rendering' functions, a way to change the appearance of a view focusing on the change of single state properties.


Great post, thanks.

For some reason the css doesn't get applied to your page when I look at it in firefox.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: