Developing with Ember: Sagas as a Service Alternative

Technically services in Ember are just singletons in an application instance’s DI container. They are often used to own state and handle effects because they live for the life of the application instance, they have access to the container and can inject or be injected, and they can be stubbed when testing.

But, let’s be honest. Services can be kind of clunky.

  • Usually they are defined as classes, not the most friendly of abstractions for composition and reusability
  • While they can import more reusable JavaScript, the service definitions themselves are not useful outside the context of an Ember app and container.
  • The designation “service” is itself not very meaningful because they can serve many different purposes and their specific interfaces are arbitrary.
  • A project can become quickly overwhelmed with many, often similar services. (For example, if you are having services act like MobX stores. Or, I have seen multiple projects use services as an additional layer of abstraction on top of the Ember Data Store, with different services for different model types.)

In this article I want to propose an alternative pattern for handling effects including updating state.

Pub/Sub, Flux, and Redux

Rather than in this article going in depth about the virtues of pub/sub, I will instead point to the following article by Eric Elliott: “Mocking is a Code Smell” (look for the section “Use pub/sub”).

Flux is an architectural pattern popularized by the React community. And the implementation/tool Redux helped further its popularity.

Flux is often diagrammed as a one-way flow of actions from the view to handlers, and of data from handlers to the view.

But I would like to instead diagram it here in terms of pub/sub (which is actually more representative of how it is implemented in Redux).

Pub/sub flux. Additional description in article.
Pub/sub flux. Additional description in article.

The main pieces are the view, middleware for referencing the environment, a state owner for consolidating state change logic, and an event bus that connects the other three.

For the benefit of the vision impaired and those who would prefer to just read, the view emits actions; the middleware consumes the view’s actions and makes needed references to the environment (e.g. API, storage, etc…), then emits additional actions to prompt state updates; the store consumes state update actions and then emits the actual state updates; the view consumes state updates and re-renders accordingly.

Enter Sagas

Redux-saga is just one of several middleware tools and patterns that have emerged alongside Redux. An early leader and predecessor to redux-saga is redux-thunk.

But, the challenge with redux-thunk is it relies on promises. (Ember Data similarly relies on promises.)

I could go in to detail about the frustrations I have personally experienced working with promises from stateful entities like components, helpers, routes, controllers, and services, but I will instead simply point to the following Twitter thread from Dan Abramov.

See also the thread to which the quoted tweet belongs.

Dan only refers to components, because they are the abstraction React provides. But, in Ember helpers, services, routes, and controllers are all stateful entities with lifecycles that can make asynchronous code that relies on promises challenging to reason about and write correctly.

Ember-concurrency helps. But we can also simply forgo promises.

Why Sagas?

There have already been an extensive number of articles published about the benefits of sagas, and how to write, test, and use them. So, I won’t go in to detail here.

Unfortunately, like services sagas are framework specific. That is, you need a special execution library to evaluate them.

But, sagas as integration and effect handling logic still have many benefits over services.

The following are just a few benefits. Sagas, which use generators, are more precise than classes. They are easy to compose. They easily interoperate with functions, even functions that return promises. Sagas are also easy to test.

Redux-saga provides many utilities for declaratively describing integration logic.

Pub/sub and Sagas in Ember

Let’s talk about how pub/sub and sagas might work in an Ember app.

Before I begin I also want to give a quick nod to the selector pattern and reselect. Basically, the goal of selectors is to de-couple the shape of the data in the store from the code that is consuming that data. They are kind of like computed properties that aren’t attached to a specific class.

For more information about selectors and their benefits see Eric Elliot’s article “10 Tips for Better Redux Architecture” (see tip 9, “Use Selectors for Calculated State and Decoupling”).

Template helpers

As shown in the flux diagram above, the view simply needs to emit actions and subscribe to state updates. This can be accomplished with two simple helpers: dispatch and select.

{{! todos.hbs }}
<h1>Todos</h1>
<NewTodoForm @onSubmit={{dispatch "new-todo"}}>
<ul>
{{#each (select "todos" filter=this.selectedFilter) as |todo|}}
<Todo @todo={{todo}} @onMarkCompleted={{dispatch "mark-todo-completed"}} @onDelete={{dispatch "delete-todo"}}/>
{{/each}}
</ul>
<TodoFilters @onUpdate={{mut this.selectedFilter}}/>

Behind the scenes is an event bus service that ties everything together. The event bus service or an application initializer registers the middleware and state owner. Then the service is injected in to the dispatch and select helpers.

The dispatch helper emits the given action with the provided data. The select helper subscribes to state updates, applies the given selector, and updates its computed value as the value of the underlying state updates.

Even with these helpers making it incredibly simple to reference application state or emit actions, it is still important to limit which components include these helpers, because they represent an integration point, and thus have limited reusability and are a bit more complicated to test. (See “Presentational and Container Components” by Dan Abramov.)

JS Decorator

While the mentioned template helpers should go a long way, it would be careless to assume action dispatches and value selections never need to occur from a JS file.

The existing pattern for injecting services should be sufficient for dispatching actions: this.eventBus.dispatch(myActionCreator(...args)).

As for subscribing to state updates, for all the frustration I expressed about working with promises, subscriptions can also be tricky. (Although tools like ember-lifeline help.)

Decorators to the rescue.

@select(getTodos)
todos;
// or if the selector requires arguments
@select(getTodo)
getTodo;
get todo() {
this.getTodo(this.todoId)
}

Disclaimer: this is fairly straightforward with EmberObject extensions. But, I haven’t tried it with Glimmer Components and tracked properties. I assume it’s still feasible.

But, I need a Promise!

Sometimes you still need a Promise. For example, what if you want to select a value that’s not yet available in a Route’s model hook?

I haven’t yet had to implement it, but it is trivial to write a method either in the event bus service or a service that injects the event bus service to create a promise from a selector that resolves when the selected value is either defined or updated. (Similar to toPromise from RxJS.)

To Redux or not to Redux?

Redux seems like an easy way to get started using sagas and pub/sub. But, a few aspects of Redux run counter to how Ember works.

I cannot yet speak from experience, but I think it would be worth challenging some things that are taken for granted when using Redux with React.

Immutability

React embraces the immutability encouraged by Redux. But, Ember instead embraces mutable observables.

I think what is more key to the value of Redux is having a dedicated state owner and a single place where state is updated.

If there are concerns about state being mutated from elsewhere in the app the store could expose a read-only proxy rather than the raw store data.

Pub/Observe

Redux communicates state updates via pub/sub. But, Ember provides facilities for observing updates of mutable observables.

I haven’t fully thought through the details, but it seems possible to re-work selectors a bit to have them reference an underlying observable state, and let Ember’s built in mechanisms for propagating updates do the rest. (It seems like tracked properties in particular could make this easy.)

Conclusion

I’m not advocating for replacing all services with sagas. But, I do think sagas are a preferable alternative to a whole class of services.

When used in conjunction with pub/sub, sagas can clean up and help organize application code. They can help prevent common mistakes made when handling promises from Ember framework class extensions. And, they can make code much easier to reason about and test.

If this sentence is true, I like cherry tomatoes.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store