Skip to content

Instantly share code, notes, and snippets.

@tomdale
Last active December 22, 2018 08:27
Show Gist options
  • Save tomdale/351499a6e5f53008fd41cbd5b767f9dc to your computer and use it in GitHub Desktop.
Save tomdale/351499a6e5f53008fd41cbd5b767f9dc to your computer and use it in GitHub Desktop.
Ember Component Lifecycle Hooks Survey

Component Lifecycle Hooks Survey

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.)
  5. 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.

Data Fetching

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);
  }
}

Argument-Based Data Fetching

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);
  }
}

Resource Setup and Teardown

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();
  }
}

Third-Party DOM Libraries (D3, jQuery UI)

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();
  }
}

Measuring DOM Element Dimensions

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) {
    // ...
  }
}
@bendemboski
Copy link

Out-of-Template Display State

The most common case is adding a class to the body element to support in-page CSS such as applying height: 100% or overflow: hidden to the body. The general case is modifying any state that is required to render a template correctly when that state cannot be controller from within the component/template.

Component Prevalence: Very Rare (usually only one or two general purpose components are all that's needed)
App Prevalence: Common (especially with the increased prevalence of display: flex, I think many apps use this pattern)

didInsertElement() {
  document.body.classList.add('.my-class');
},

willDestroyElement() {
  document.body.classList.remove('.my-class');
}

See https://github.com/ef4/ember-set-body-class

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment