didUpdateAttrs in glimmer-components or how to implement an asynchronous getter

Hi

I try to get used to octane and so far I’m quite impressed. But I’m facing a problem where I even don’t know if I’m searching in the right direction - so maybe the topic is misleading.

I have a property I need to get from store depending on two arguments. If the property isn’t found I need to create a new one.

Up to now I have this code:

import Component from '@glimmer/component';

export default class MyClass extends Component {
	@inject store;
	@tracked property;

	async getOrCreateProperty() {
		let p = null;
		try {
			p = await this.store.findRecord('property-model, ...);
		} catch {
			p = this.store.createRecord('property-model', {
				argument1: this.args.argument1,
				argument2: this.args.argument2
			});
		}
		this.property = p;
		return p;
	}

	constructor(owner, args) {
		super(owner, args);
		this.getOrCreatePropertyUser();
	}
}

This works so far. But what should I do, if the two arguments change? With the classic components I could just implement didUpdateAttrs. But combining async and get doesn’t work and returning a promise in a getter doesn’t work either.

Or do I need a completely different approach?

Thanks for your help.

There are two basic approaches I would take here. The first is the simpler; the second the one I actually recommend.

  1. You can use ember-render-modifiers to get the same basic hooks as you had in classic components via modifiers:

    {{! the component definition }}
    <div
      {{did-insert this.getOrCreateProperty}}
      {{did-update this.getOrCreateProperty}}
    >
      {{! other stuff }}
    </div>
    
  2. You can restructure your data flow a bit. Usually when I see a situation like the one you’ve described, I try to think through how this could be restructured in terms of data that gets passed into the component, rather than the component having both argument-driven state and internal state driven by services (including the store). One option for now might be to extract this conditional logic into a class-based helper which you call at the invocation site:

    <MyClass @foo={{loadOrCreate this.targetProp this.targetId}} />
    

    Then the helper would be async. The key to making that work well would be having the component be able to deal with the async argument. I tend to want to go one step further and separate that concern as well, using a data-loading helper which is responsible for rendering out the state of a given Promise. If you did that, you might have something like this:

    {{#let (load
      (loadOrCreate
         type=this.targetProp
         id=this.targetId
         props=(hash argument1=this.argument1 argument2=this.argument2)
      )
    ) as |result|}}
      {{#if result.isLoaded}}
        <MyClass @foo={{result.value}} />
      {{else if result.isLoading}}
        Loading...
      {{else if result.isError}}
        Whoops! Something went wrong!
      {{/if}}
    {{/let}}
    

While that’s a bit more involved at first blush than just doing effectful operations via the old didReceiveAttrs hook, it has a couple nice benefits:

  • Every piece of it is totally reusable. The {{load}} helper (which I can publish a version of to a GitHub Gist if you like!) can be used for any Promise-based load operation. The {{loadOrCreate}} helper likewise can work on anything like this.

  • The component doesn’t have to know anything about the store or even about asynchrony. It just gets to go back to being a function of its arguments. Or, you could also make it take the result as its argument and handle the asynchrony, but either way you have a nice bit of control as a result, and it gets way easier to test.

1 Like

Addendum: this may end up being a place where @pzuraq’s proposed @use and Resources tools would be handy, too!

Thanks for your reply.

Your first approach works and looks simple, but I’m also not sure if I like it. The way I would use these modifiers reminds me of an observer, except now there is some logic is in the template - so it’s even worse. But hey it just works :upside_down_face:

I’m not sure I understand your second approach. The sole purpose of this component is to provide the CRUD operations for this model. So there is also a save and a delete method and some stuff for error handling. Would you put these methods in the helper class too? If you have a finished version of your load helper, it would be really nice if you could publish it somewhere.