How to force re-render a Glimmer component

Hello :wave: ,

Is there a way to force re-render a Glimmer component?

What I have is an Image component, which displays a placeholder image, while the actual one is being downloaded, which later gets replaced by it. The fetch of the image is triggered by a {{did-insert}} action.

This is all ok, but since I am relying on that hook, I have to do some manual work when the passed image is changed. A re-render will solve this.

Here is a solution that I came up with

{{#each (array this.photo) key="path" as |photo|}}
  <Image @photo={{photo}} />
{{/each}}

This works really well, but it feels hacky. I wonder if there is a built in way to do it.

This is a really common challenge people have as they learn Octane idioms and Glimmer Components, so I’m glad you asked it!

The key here is moving away from theimperative mode we tended to reach for in Ember Classic, where we would respond to a lifecycle hook or event by telling the app “create some new state” or “update in this way” and moving to a more truly one-way data flow paradigm. You can think of this as really going all in on Data Down, Actions Up. That means we need to transform your approach with did-insert into something that defines everything in terms of data.

To get there, we need to define what the template renders purely in terms of data derived from the arguments to the component. The result might look something like this:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

const PLACEHOLDER_URL = '...';
const FAILED_URL = '...';

class ImageData {
  @tracked state = 'loading';
  @tracked value;
  @tracked reason;
}

export default class Image extends Component {
  get imageData() {
    let data = new ImageData();
    let { url } = this.args.photo;

    fetch(url)
      .then(() => {
        data.state = 'loaded';
        data.value = url;
      })
      .catch((reason) => {
        data.state = 'error';
        data.reason = reason;
      });

    return data;
  }

  get src() {
    switch (this.imageData.state) {
      case 'loaded':
        return this.imageData.value;
      case 'loading':
        return PLACEHOLDER_URL;
      case 'error':
        return FAILED_URL;
    }
  }
}

Here’s the corresponding template (yours might be more complicated, of course):

<img src={{this.src}} alt={{@photo.alt}} />

Note that in src, we use imageData to determine what to show, and imageData uses args to determine what to load. Since we’re using src in our template, any time the photo argument to Image changes (or, if it has internal tracked properties for src and alt and either of those change) it’ll update correctly!

(If it’s surprising that we’re doing an async data lookup in a getter, which is often treated as an antipattern, see this discussion on “Guidance re: triggering tasks from component args”.)

This pattern comes up enough that my team actually built a load helper and AsyncData type to make it convenient, which can be used on either the JS or Handlebars side. Using that, this would be the whole implementation of the backing class:

import Component from '@glimmer/component';
import { load } from 'my-app/helpers/load';

const PLACEHOLDER_URL = '...';
const FAILED_URL = '...';

export default class Image extends Component {
  get imageData() {
    let { url } = this.args.photo;
    let promise = fetch(url).then(() => url);

    return load(promise);
  }

  get src() {
    switch (this.imageData.state) {
      case 'loaded':
        return this.imageData.value;
      case 'loading':
        return PLACEHOLDER_URL;
      case 'error':
        return FAILED_URL;
    }
  }
}

The template would be exactly the same!

The big takeaways here:

  1. There’s no need for did-insert in the template anymore! All the template needs to know is what src to use for the img tag, and that’s just computed as data by the class.
  2. We now have 100% 1-way data flow. Arguments come into the component, and it updates automatically just by using auto-tracking. (You would get the same effect from VDOM implementations in other frameworks, but the performance tradeoffs would be different.)
  3. Everything in this approach—including the async operation!—is just data. We’ve managed to describe the whole component flow in terms of one source of truth (or “root state”): the photo argument to the component. The two other pieces of data are derived from that source of truth (so they’re “derived state”).

If you’re curious about the load helper and AsyncData type, I posted an example this week of using it to migrate away from PromiseProxyMixin, I’ll be writing up an explanation of the philosophy behind it—which is basically just taking the ideas from this example and expanding them into something more general-purpose—this coming work week, and I’ll post a link to that on this forum when it’s live!

2 Likes

Thank you for the reply. This pattern looks nice. I usually go with a simpler approach, but always felt that I was kinda cheating.

@tracked fetchedSrc;

get src() {
  if (this.fetchedSrc !== this.args.photo.url) {
    this.fetchImage(this.args.photo.url);
    return this.placeholdeSrc;
  } else {
    return this.fetchedSrc;
  }
}

async fetchImage(url) {
  await fetch(url);
  this.fetchedSrc = url;
}

The problem that I have, is that I am not just changing the src of the img tag, but the width and height as well, depending on the actual image sizes. This results in a slight visual resize of the previous source, before it actually changes to the new one. Which is solved by doing a complete re-render.

Thanks again for the reply. I will definitely check the references that you gave.


A bit unrelated - I did not know that you can use fetch to fetch an image :open_mouth:. Which lead me to a new discovery for me - AbortController (double :open_mouth:).

Two notes there:

  • the solution I gave would handle that, because it would switch back to the loading state when given a new argument
  • you could define the height and width as getters dependent on the resolved image result as well, which would prevent their ever being applied to the wrong image

Also, you likely don’t need the previous image to be tracked!

I would definetly play with this a bit to see the result that it produces.

I will need to look into this, as I am not really sure when do getters get invalidated. For example, will the get src() get called again if the this.args.photo.alt changes?

It will if args.photo changes or if the alt property itself is tracked.

Also, it is not that the getter “invalidates.” There’s actually no magic around getters. They’re just plain JS getters, unlike in the old computed property world. By setting up getters you’re creating lazy references to access the other data in the system. They’ll be re-executed whenever a primitive that hooks into the VM’s tracking mechanics—currently just templates but soon also helpers on the JS side—consumes a tracked value either directly or by the lazy reference to it via the getter. But the only things that matter for tracking to work this way are the root tracked properties and the tracking-connected things which consume them either directly or indirectly.

1 Like