Most Ember developers are familiar with component lifecycle hooks, like
didInsertElement
, willDestroy
, etc.
As we think about future APIs for Glimmer components that improve on lifecycle hooks, we want to make sure that we're providing abstractions that are simple, performant, and ergonomic, while nudging users towards good patterns and away from potential footguns.
To that end, I'd like to ask the community to help me gather use cases that rely on component lifecycle hooks. We want to understand how they're being used in the real world today.
Please audit your own applications and share use cases as a comment below, using the following template:
### My Use Case
A few sentences about this use case, and any additional context needed to understand it.
**Component Prevalence**: Very Rare | Rare | Uncommon | Common | Very Common
**App Prevalence**: Very Rare | Rare | Uncommon | Common | Very Common
```
Example JavaScript/template goes here
```
Component Prevalence is a rough estimate of how often this use case comes up when writing components day-to-day. Is it something you encounter frequently when implementing a feature? Or is it a power tool that most developers won't have to deal with most days?
App Prevalence is a rough estimate of how often the average Ember app would have at least one instance of this use case.
These are broken out into separate figures because some use cases may be very rare in a given component, but almost every app will encounter it eventually. Other times, a use case is not that common in every Ember app, but apps that do run into it run into it frequently.
The goal here is to drive the API design through real-world motivating examples. Having example code lets us see how new API proposals will look when used to solve these problems, and understanding the frequency with which this use case pops up helps shape what areas should get more sugary, shorthand syntax.
Some guidelines:
- Avoid non-idiomatic cases. Please don't include use cases that, given
your understanding of the Ember programming model today, are better handled
other ways. For example, setting a DOM element's attribute to the value of a
component property is probably better done in the component's template, not
by running imperative JavaScript code in
didInsertElement
. - Distill the use case. If possible, also try to identify the higher-level behavior that is required. For example, "Sending an event to Mixpanel whenever the component's tracking ID changes" is great, but if you can also identify that this is more generically "Performing async behavior in response to an argument change," it's easier to group seemingly-unrelated use cases.
- Simplify the example. Please don't copy-paste a thousand line component from your app as the example. Instead, try to reduce the example down to the smallest possible snippet of code that gets the specific use case across.
- Avoid truly unlikely cases. For example, if your manager instructed
you to instrument every component in your app with a
didInsertElement
hook that fires synchronous XHR to collect analytics "because we're a data-driven company and more data is always better," that's probably not a common use case we'd want to optimize our API design for. (There's also an#ember-jobs
channel in Discord so you can get out of this situation.) - Don't worry about the prevalence estimates too much. There will be some things that are used everywhere in one app and not used at all in another. If you're not sure, just estimate these numbers based on what you've seen in your own apps/addons, or leave it out altogether.
Here are a few motivating examples I've gathered already.
In many cases, it's desirable to move data fetching out of the Route
and
into a component. For example, imagine a component that fetches the latest
weather information from a JSON API and displays it. If this data only needs
to be fetched once, this can be done in init
or didInsertElement
.
Component Prevalence: Uncommon (most data fetching is relatively centralized in a few components)
App Prevalence: Very Common (almost every app performs some data fetching in a component)
class Weather extends Component {
async didInsertElement() {
let request = await fetch('https://api.example.com/weather/10001.json');
let weather = await request.json();
this.set('weather', weather);
}
}
This one was brought up by @Guarav0 in the Glimmer Components quest
issue.
In many cases, you may want the data a component fetches to be determined by
one or more arguments that are passed to the component. That data should be
updated if those arguments change. In the above Weather
component, for
example, we may not want to hardcode the zip code and instead make it
configurable.
Component Prevalence: Uncommon (most data fetching is relatively centralized in a few components)
App Prevalence: Very Common (almost every app performs some data fetching in a component)
class Weather extends Component {
async didReceiveAttrs() {
let zipCode = this.zipCode;
let request = await fetch(`https://api.example.com/weather/${zipCode}.json`);
let weather = await request.json();
this.set('weather', weather);
}
}
This is for cases where you have some behavior that should start when the component is rendered, and must be torn down when the component is destroyed to avoid memory leaks, performance problems, or other bugs.
Component Prevalence: Rare (usually abstracted over due to ease of getting
setup/teardown wrong)
App Prevalence: Common (may not be needed in smaller apps, used frequently
in apps with real-time features or with sophisticated UI requirements)
class StickyHeader extends Component {
didInsertElement() {
this.onScroll = () => {
// ...
};
window.addEventListener('scroll', this.onScroll);
}
willDestroyElement() {
window.removeEventListener('scroll', this.onScroll);
}
}
class StockTicker extends Component {
@service socket;
didInsertElement() {
this.socket.open();
}
willDestroyElement() {
this.socket.close();
}
}
This is similar to "Resource Setup and Teardown" but with the additional requirement of needing to reflect changes from Ember out into a third-party library, and in some cases listening for changes in that library and reflecting them back to Ember.
Component Prevalence: Rare (usually abstracted over due to ease of getting things wrong)
App Prevalence: Rare (going out on a limb to say this isn't needed by most modern Ember apps, due to diminished popularity of jQuery plugins, and most DOM interaction is better handled via components/templates)
class StockTicker extends Component {
items = ['a', 'b', 'c'];
didInsertElement() {
let svg = d3.select(`#${this.elementId} svg`);
this.set('svg', svg);
this.updateSVG();
}
didUpdateAttrs() {
this.updateSVG();
}
updateSVG() {
let { svg, items } = this;
let text = svg.selectAll('text').data(items);
text.enter().append('text')
.merge(text)
.text(label => label);
text.exit().remove();
}
}
Sometimes you need to lay out UI elements relative to some DOM element, but you don't know the dimensions of that element ahead of time. For example, you may want to know the height of an element that contains text content, or position a popover relative to some other element on the page.
Component Prevalence: Rare (most content is laid out with CSS, DOM measurement is avoided when possible due to performance impact, often abstracted by addons due to difficulty of avoiding bugs)
App Prevalence: Uncommon (usually only needed in apps with more sophisticated UI design where CSS solution isn't available)
class Popover extends Component {
didRender() {
let targetEl = document.querySelector(this.targetSelector);
let bounds = targetEl.getBoundingClientRect();
this.positionPopoverRelativeToBounds(bounds);
}
positionPopoverRelativeToBounds(bounds) {
// ...
}
}
Rendering before data is received / ready
In this component, after render, a loader is shown, because the rendering component / context doesn't yet have the data that this component needs, represented by
hasDeliveryConfirmations
. IfhasDeliveryConfirmations
never becomes true, this component then renders a timeout message.This could be common in optimisic UI scenarios, where the data in flight is not managed by an optimistic store. I could see a pattern where a component is rendered with expected data, and then has an indicator / loader that shows before the data is successfully committed.
Component Prevalence: Rare (most data is known ahead of render)
App Prevalence: Common (every app I've seen has scenarios where UI is rendered before the data is ready for it in order to appear snappy)
full code: https://gitlab.com/NullVoxPopuli/emberclear/blob/master/packages/frontend/src/ui/components/chat-history/message/delivery-confirmations/component.ts