It's been a few years since Ember.js has introduced major new conventions for application structure into its core APIs. If you take a look at the Ember.js Guides, you're introduced to the same URL-centric application structure that's been in place for about 4 years: you map out the states of your app using
Router.map, you load data in your
Route.model hooks, the loaded data gets passed to your controllers, and, at the "leaves" of your application, you can build and render reusable
There are countless benefits to adopting Ember's convention-over-configuration architecture for building your applications. To name just a few:
- URLs + browser-based navigation Just Work™
- Ember developers can drop into a new Ember codebase and be immediately productive
- The addon ecosystem can offer powerful solutions under the assumption of these shared architectural conventions
In recent years, Ember has invested heavily in ember-cli and the Ember addon ecosystem so that new architectural ideas can be built, tested, and refined without requiring changes to the core Ember codebase. This has enabled the Ember community to innovate, experiment, and build shared solutions without being blocked on the necessarily slow, conservative, and methodical process of proposing and introducing code changes to core Ember.
I haven't been particularly well-served by the core Ember Routing API since around early 2014 when I started building mobile apps (using Ember + Cordova) at FutureProof Retail. There are numerous reasons for this, some of which I'll mention below, but rather than enumerate a long list of complaints, I'd like to introduce a name to the general category of application that is still surprisingly hard to build in Ember, and that name is
Rideshare apps (like Lyft and Uber) share a well-known flow: the user signs in, geolocates, sees a live map of nearby drivers, requests a ride, is assigned a driver, gets picked up, and eventually arrives at the destination.
As simple and ubiquitous as this app flow is, I feel reasonably confident that if you commissioned the world's best Ember developers to build
ember-rideshare -- a thought experiment rideshare mobile app built with Ember.js -- they would fight many of Ember's deep and stubborn conventions every step of the way, and even if they managed to build a codebase that resembled clean and idiomatic Ember, it would only be by employing every pro-tip in the book, monkey-patching, and a using a series of mixins and
.reopens to arrive at a sort of proprietary uncanny valley Ember that would likely break between upgrades.
Again, I'm not here to complain, but to rally the community behind some common vocabulary as to what's missing from Ember so that new innovations, whether they originate from the addon community or from changes to core Ember, might consider this family of use cases that I feel has been largely unaddressed. It's my hope that when evaluating an Ember RFC for some new feature, for instance a routeable components RFC, we consider whether the proposed feature would play nicely within an
What's so difficult about
Let's talk about a few present-day pain points, some of which could be addressed and fixed very directly and specifically by an RFC, and some of which might have deeper Ember-wide implications.
Most mobile apps use stack-based navigation, where you push/pop frames to/from your application's "navigation stack"; you can't really express this kind of routing in vanilla Ember and expect to 1) be able still use
model hooks and/or 2) preserve the stack in the URL so that it isn't lost when you refresh the page (this is mostly just useful in development, but for deep-linking too).
At my company, we did end up somewhat hackishly building out our own concept of stacked routing (that I demo here), but generally speaking it's still pretty difficult to extend the Router's baked in assumptions about the kinds of apps/navigation you're trying to build (at least relative to other parts of Ember that are more open to experimentation via Ember Addons).
URLs aren't so first class
While it's nice that Ember makes it so easy to build URL-accessible apps, in an app like
ember-rideshare, only about 20% of the pages/frames are accessible at any given time depending on where the user is in the rideshare process. For instance, if the the user is in the middle of a ride, the user shouldn't be allowed to navigate to the
/request-ride route until the ride is finished.
Accounting for this amounts to a lot of route entry validation code that usually goes into
beforeModel hooks, which could possibly be shared between multiple routes via some mixin that defines a shared
beforeModel, or by nesting routes with common validation under a shared parent route with an empty URL. But in a server-driven app like
ember-rideshare, there are problems with this approach:
In addition to route entry logic, there is also "can I still be in this route" logic that needs to re-fire every time the server sends an update to the ride state (e.g. you can't stay in
/cancel-trip once the server tells you the ride is finished). This logic can't just live in the one-shot
beforeModel, but probably needs to live in some centralized service that handles / delegates all state updates from the server, as well as local requests from the user to navigate to different routes. As far as I know, the Ember Router doesn't really give you an easy way to do this, and kind of makes you feel bad or guilty if you try.
Client-centric APIs make server-driven state changes hard
A lot of Ember's API makes the subtle assumption that most state changes are going to originate on the client side: the user clicks a link, submits a form, clicks the back button, etc, and these actions cause application state to change.
When it can be assumed that the user is the originator of most state changes, APIs can be more "encapsulated"; you can store button/tab state right on a component, which can be very convenient, but only when no other parts of the app ever need to change that button/tab state when the component is unrendered (though ember-state-services can help with some of these cases).
A subtler example of this encapsulation is the issue with "can I still be in this route" validation logic I mentioned above;
beforeModel is handy for route entry validation when it's just the user clicking around, but once it's possible for an external source (like a server) to invalidate access to a route, you can't really keep using
beforeModel for validation without duplicating that logic between
beforeModel and wherever you handle updates from the server.
These may seem like minor issues with simple-enough solutions, but it's 2017 and I still fight these issues on the daily and I doubt I'm the only one.
A bold stance
Back in 2013, when most other frameworks/libraries treated routing as an afterthought (hence the epidemic of SPAs that either had no routing or were rife with broken back buttons and other routing issues), the Ember core team made a strong case to developers that 1) you're breaking the Web if you ship an app that doesn't support URL routing / navigation and 2) if you start building an app without considering URLs up front, you're in for a world of pain when you inevitably add routing to your app. I think it's time that Ember took a similar stance on server-driven apps: if your app isn't receiving state changes from the server today, it probably will tomorrow, and when it does, you're in for a world of refactoring pain if you've been building around APIs that treat server-side state changes as an afterthought.
I'm hoping that by introducing
ember-rideshare as a thought experiment, I might have an easier time convincing the community that this is an area where Ember needs to rethink some central tenets and assumptions subtly baked into our APIs. If you can think of another style of app that is just as familiar as rideshare apps and succintly represents a class of problems that the next generation of Ember apps is likely to face, please let me know.