Adopting better Ember patterns
Recently, I’m quite enjoying refactoring and enhancing Canopy’s Ember codebase. I’ve been telling everybody I have 3 tips to share, so I thought to note down 5 of my recent anti-pattern observations so that I could refer back to them when working with my team. Because like every software developer, I like to under-promise and over-deliver :stuck_out_tongue_winking_eye:
1. Chatty templates - repetitions
This one is my favorite because it’s the easiest to fix!
Handlebars markup is already quite a bit to look at, and maybe that’s why, it’s easy to not notice when entire chunks of it is being repeated.
The easiest way to demonstrate this is to use an example from a recent enhancement made by a colleague.
Before you commit a lengthy one, watch out for opportunity to shorten a Handlebars template with repeating content. Of course, this will require a corresponding property in the component file, but you already know that.
2. Chatty templates - excessive logic
Mostly because we don’t anticipate it, we often start introducing logic into our handlebars, which quickly turns into a mess of nested
Within Canopy, there is a concept of a regular and switched user - if an admin logs in and decides to view as another user, the app is in switched user mode. When displaying the user’s net worth value, we also need the currency. Depending on whether a user is in switched user or regular mode, the currency property is derived from different locations.
When coding the template in the context of a feature, it’s easy to get lost in the interface requirements, and end up with code that looks like this:
That’s what I like to call a chatty template - consequently, as a reader of this piece of code, we are worrying about more things than necessary.
A good idea is to take the base currency section off the handlebars and to move it within the controller or component’s logic. In this case, I break it down as follows:
If you find yourself struggling with a mess of nested if blocks, it’s time to move that logic out of the template.
Often, you’ll also realize that when you do move the logic out, it turns out to be something that can go into an application-wide Service, because you’ll find utility for it elsewhere.
3. Abusing AJAX / Fetch for API requests
As of this writing, this RFC is open, so the following tip will probably change slightly in the near future.
Far too many times, I’ve observed the usage of fetch / AJAX being used for making API requests, when
one of Ember Data’s
BuildURLMixin methods would have been more appropriate. e.g within Canopy’s codebase, we were repeatedly evaluating the relevant accounts we wanted to display a chart for, and built an API request string manually by concatenating several params and then plugged them in.
A more elegant approach would be to build a base adapter (say the
ApplicationAdapter) that handles app-wide request parameters, and then to extend a route specific adapter from it, that adapts to whatever format of requests we may be interested in.
Try and avoid vanilla AJAX / Fetch requests when an Ember Data method is available.
4. Abusing services for storing everything
Services are singletons, which means once instantiated, you cannot have another instance. Due to this nature, they’re great for app-wide state maintenance activities, like maintaining session data (logged in user details, timeout information, etc).
As with any other singleton concept, Services have the tendency to be abused for avoiding the passing around of dependency. Quoting from this StackOverflow answer (which is great btw!):
Making something global to avoid passing it around is a code smell
I observed that we tend to store state for a specific route on a route-specific service, as a means to free the controller of state information on that specific route. e.g for a controller
holdings, it’s often tempting to store away state information within a
holdings-state service. However, this decision often bites back when we forget to “flush” it on exiting the route, and a common bug arises - unexpected state when the route is being revisited.
Another consequence of stowing away data into a service means often there is the necessity to reconstruct parts of data that is already available on the service again on the controller, because it was not “readily visible”.
Therefore, a good rule of thumb → absolutely utilize Services, but do so when you’re looking to share state in a way that it doesn’t make sense to be passed around.
This will ensure you’ll avoid the mess of too much dependency passing around, and benefit from making everything “global”.
5. Indulging in observers
As your codebase gets complex, observers can lead to unexpected behaviour. Well the behavior isn’t really unexpected in the true sense of the word, if you know completely what’s happening under the hood. More often than not, using observers sort of takes away understanding from you, onto the framework, and when unexpected scenarios occur, it gets difficult to reason about the logic your code follows.
When writing ember, imagine each observes you manually write, costs you $10.
In practice, I’ve found that 98% of the time an observer is used to solve a problem, it can be better solved by:
- Introducing a computed property instead
- Using Component lifecycle hooks
- Re-thinking your approach in terms of DDAU pattern (more on this soon)
(The remaining 2% is mainly because I don’t remember every single time I’ve thought of replacing an observer
A recent example comes to mind from within Canopy, where we had to monitor for data updates to a chart. An observer was set up to watch for any updates, and run a chart specific method. We were able to improve this by leveraging a Component lifecycle hook instead.
You can make do without an observer. Look for a different approach.
At times, the amount of code you’re required to change can feel phenomenal, just to get rid of an observer. I know from experience that it’s worth it, because on one occasion, getting rid of observers and implementing a DDAU approach was the only way to get Canopy’s data-heavy holdings page to be performant enough to work across all environments we wanted to support.
6. Not adopting unidirectional flow via DDAU
At the heart of the making application code more predictable and “pure” is the concept of unidirectional or one way flow, or DDAU as it’s better known in Emberspeak - Data Down, Actions Up. For me, this idea became super duper clear when I started to play with React three weeks ago, but there have been discussions about the case for unidirectional design within Ember apps forever.
Essentially, you want to follow these ideas:
- Maintain state (data) at the topmost level among components that use it, from where it is distributed downward. Good thing about keeping it not at the absolute topmost (say Controller), but only at the unit of storage has the right amount of oversight is that as you destroy objects, state is cleaned up for you ⇒ no leaky state.
- Any mutations to state are notified by child components to their parent components all the way up to the level maintaining state. These notifications can be achieved by passing actions up.
- For the passing actions up, traditionally,
sendAction()s were used, but with the availability of closure actions, that’s preferable.
For example, if you’re interested in User state → store it within a Service. Toggle UI widget open → likely on that UI widget. Upload progress → likely on the upload object.
I was going to share code to demonstrate closure actions, but I found this post summarizes it well.
Remember, always, Data Down, Actions Up.
In my attempt to under-promise and over-deliver, that was not 3, not 5, but 6 techniques to guide you through design decisions concerning the use of Ember! :fireworks:
Repeat with me one last time, for a quick recap:
- Avoid repetitive markup within Handlebars templates through the use of
- Avoid messy nested conditionals by taking them off the template
- Avoid abusing AJAX / fetch, in favor of Ember Data methods
- Avoid abusing Services, use them to store shared state, not to avoid passing around dependencies
- Avoid indulging in observers, and utilize computed properties, lifecycle hooks, and DDAU
- Maintain state at the top, mutate at the top, send data down, and pass actions up