In late 2019, the Ember.js Octane edition was released which included a new way of writing components: Glimmer components. Components now extend the component class from the Glimmer package instead of Ember. Besides this minor difference in importing there’s a large difference in functionality. This article will go over the differences, reasons why you would want to upgrade, and an upgrade strategy to tackle this in large codebases.
Classic vs. Glimmer
Glimmer components can be seen as a slimmed down version of classic components. Most lifecycle hooks were removed. Arguments are scoped and built upon auto tracking reactivity from Glimmer. There’s no more HTML wrapping element. They use native class syntax. And a lot of classic leftovers were cleaned up.
The following example implements a component which copies the text passed as argument to the clipboard when clicking a button. When using classic Ember components, it could be implemented as follows:
// copy-to-clipboard.js
import Component from '@ember/component';
import { set } from '@ember/object';
export default Component.extend({
isCopied: false,
actions: {
async copyToClipboard() {
await navigator.clipboard.writeText(this.text);
set(this, 'isCopied', true);
}
}
});
<!-- copy-to-clipboard.hbs -->
<button {{action 'copyToClipboard'}}>
{{if isCopied 'Copied!' 'Click to copy text'}}
</button>
The same component using Glimmer would look like this:
// copy-to-clipboard.js
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { action } from '@ember/object';
export default class CopyToClipboard extends Component {
@tracked isCopied = false;
@action
async copyToClipboard() {
await navigator.clipboard.writeText(this.args.text);
this.isCopied = true;
}
}
<!-- copy-to-clipboard.hbs -->
<button {{on 'click' this.copyToClipboard}}>
{{if this.isCopied 'Copied!' 'Click to copy text'}}
</button>
The Glimmer component can make use of decorators as it uses native class syntax. There is a clear separation between arguments passed to the component (here text
) and the local state (isCopied
). Regular assignment expressions can be used to update state that should trigger template rerenders thanks to Glimmer auto tracking. And there's a lot more improvements which aren't illustrated in this small example.
Why migrate to Glimmer components?
Every code migration requires engineering time which can not be used to build new products to sell to customers. So for a business to invest into refactors and migrations there has to be another benefit. Classic components in Ember are still supported in the latest major version, so why upgrade? The following benefits for us made it worth the trade-off.
One way of doing things
Glimmer components for new code became the standard practice since the release of Ember Octane. This caused our codebases to contain two component types. This adds extra mental overhead when working in codebases which contain both. You have to be aware of which type of component you’re working with and make changes accordingly. For people new to Ember this can be extra confusing.
Closer to native JavaScript experience
Glimmer components contain very little Ember specific code practices compared to classic components. This makes it easier for people to get started in Ember coming from a different background. Every JavaScript developer should be able to get started in our codebase and get up to speed relatively quickly.
Rendering performance
The previous points are nice from a developers perspective. There’s, however, also a benefit for customers. Glimmer components render substantially faster than classic components.
TypeScript support
TypeScript has proven it’s here to stay in the wider JavaScript ecosystem. It has risen in interest and kept its place as the most popular JavaScript flavour.
In 2022 Ember acknowledged official TypeScript support with a dedicated core team. Glimmer components unlock the full potential of TypeScript support in Ember. Template type checking with Glint is also under active development. Exciting!
Future-proof a codebase
Ember.js development doesn’t stagnate. Progress is already being made for new improvements to the current component model. The RFC for first-class component templates has been accepted and merged in 2022 and will provide new benefits to Ember users. By first adopting Glimmer components, we’re prepared for what’s coming next.
Migration strategy
While you could jump straight in and start migrating every component one by one, we decided to go for a different strategy. For smaller codebases migrating components one by one can be a feasible approach, but this can be cumbersome for large codebases (think 100K+ lines of code). This effort is way too large for a single person and has too many side effects. This is why we broke up our migration effort into nine milestones.
1. Native JavaScript class syntax for components
Historically Ember used object syntax to define components. As class syntax matured in JavaScript in general, it also became the standard for Glimmer components. Classic components in Ember provide support for both object and class syntax. This makes switching to class syntax a great first step towards Glimmer components.
Ember provides a codemod to convert object syntax to class syntax. This has saved us a tremendous amount of time. By doing this our development experience also greatly improved.
2. No implicit this
Arguments in Glimmer components are bundled in the args
object. This avoids clashes with custom defined properties in the component’s own scope and creates a clear distinction between properties defined locally and passed arguments.
Glimmer component templates reflect this by using the @
prefix when using arguments and the this.
prefixes when accessing properties of the backing class. This way of working is also supported in classic components, even though arguments are in the same scope as local properties. This means the migration is non blocking, and luckily there’s a codemod available for this as well. The codemod however can’t make a distinction between arguments and local properties, and is something that will be cleaned up in a later phase.
3. Getting the simple components out of the way
By reviewing all components and checking which used none or limited of the classic component features, we were able to identify a set of components which were easily migrated to Glimmer. Examples are components which did not have any JavaScript logic, as Glimmer introduced the concept of template-only components which work without an explicit backing class. This was low hanging fruit and by getting them out of the way directly we avoided unnecessary overhead of the other phases.
4. Remove outer HTML semantics
Classic components have a wrapping HTML element which doesn’t exist in Glimmer components. A first step to prepare for this removal was to get rid of all properties that have an impact on this wrapping element. In most cases this usage was the classNames
attribute which added CSS classes to the wrapping element.
Converting was done by adding these properties directly in the template of the component.
5. Making components tagless
Wrapping elements of classic components can be removed by setting the tagName
to an empty string, hence the name “tagless” components. The @tagName
decorator from the ember-decorators package can be used to do this. This makes it easy to spot and clean up in a later phase.
Making the component tagless in this phase still introduces breaking changes which we fixed together with adding the decorator.
A common pitfall we noticed was that attributes on an Ember component had no place to be set and were dropped. In Glimmer you explicitly need to tell where the passed attributes have to be placed. This can be done by using the ...attributes
syntax. Often this caused styling bugs as classes or id’s weren’t set. Our visual tests came in useful to detect these issues. If you’re interested in how we set up visual testing, check out our talk at EmberFest 2022.
A second issue was that lifecycle hooks that depended on this wrapping element no longer got invoked. Those lifecycle events contain the Element reference, e.g. didInsertElement
. To migrate these we made use of the render-modifiers package. Ever since Glimmer and Octane, there are new ways to encapsulate this logic like using the constructor and destructor, writing custom modifiers, or using resources. For the sake of limiting the scope we opted to keep this a separate effort.
6. Removing Mixins
Mixins were a way to share common code across different components. In Glimmer they’re no longer supported. We reviewed our mixins and listed a way of restructuring them as in most cases mixins could be replaced with a more specific way of sharing code.
Common cases were template formatting logic which could be made into a helper, shared constants which could be moved to a separate file, and utility functions which could be separated as they didn’t require the Ember context. For usages that didn’t fit nicely in any of the standard ways of sharing code, we opted for creating custom class decorators as described in “Do You Need EmberObject?” by Chris Garrett.
7. Removing deprecated lifecycle events
In phase 5 a subset of deprecated lifecycle hooks were already removed. There are still others left which are not bound to the wrapping element, like didRender
, willUpdate
and others. Removing these lifecycle events can be done using a similar strategy as used in phase 5. Generally they can also be replaced with native getters.
8. Removing observers
Usage of observers has been discouraged in Ember for a long time. They were often overused when a better alternative was available, could cause performance issues, and were hard to debug. With Glimmer components, the @observes
decorator is also no longer supported.
Refactoring of observers can be non-trivial as it requires you to think of your state updates differently. Rather than reacting to changes, Glimmer introduced autotracking which allows marking of inputs which should trigger UI updates. Some usages can be replaced by working with getters and autotracking. In other cases the did-update
modifier of the render-modifiers
package can be used as a replacement. Writing custom modifiers is also an option here.
9. And finally … extending from Glimmer!
Now that all classic specific features have been removed, it is time to extend the Glimmer component base class instead of the Ember classic one.
By making this change, arguments will move to the args
object instead of the component’s local scope. Usages in the backing class have to be adjusted to use this step in between. One edge case to take into account is that by the change of this scope, they no longer override default values in the component. This can be resolved by writing a native getter which returns the argument from the args
object and falls back to a default in case the argument is not passed.
Likewise, argument usages in the template also have to be updated to indicate the difference in scope. The @
prefix has to be set for arguments as the codemod didn’t handle this like mentioned in phase 2.
Finally, the tagName
decorator added in phase 5 can be removed as Glimmer components are always tagless.
Conclusion
This article provided a strategy to migrate large Ember codebases from classic to Glimmer components. Following this component migration ensures codebases don’t get stuck in the past. Even better, they unlock modern features Ember provides and new ones being worked on at the very moment!
Top comments (1)
That's great! Could you please send me a direct message to manhng83@gmail.com? I need your help, thanks so much!