ember.jsember-octaneglimmer.js

How to update nested state in Ember Octane


So my situation is as follows: I got a component with a couple of input fields that represent a contact and are filled with data from a service:

  @service('contact-data') contact;

Each field stands for one property that is accessed via

{{contact.properties.foo}}

I have the properties saved as a JS object to easily filter out empty fields when using them and I tracked it with @tracked like so:

export default class MyService extends Service {
  @tracked properties = {id: 0, foo: "", bar : "abc", };

  @action updateProperty(name, value) {
    this.properties[name] = value;
  }
}

However, the properties do not re-render properly in the component and the textfields do not get updated.

I'd appreciate any help with this! Thanks!


Solution

  • Any time you have a bunch of nested state like that which needs to be tracked, just tracking the top-level object won't cause updates to the internals of that object to propagate out. You need to track the internal properties, or you need to reset the whole object you're tracking.

    You have basically two rough options for dealing with updates to those internal properties:

    1. If the object has a well-known shape, extract it into a utility class which uses @tracked on the fields, and instantiate the utility class when you create the service. Then updates to those fields will update.
    2. If the object is really being used like a hash map, then you have two variant options:
      1. Use https://github.com/pzuraq/tracked-built-ins, if you don't need IE11 support
      2. Do a "pure functional update", where you do something like this.properties = { ...this.properties, foo: newValue };

    Of these, (1) is pretty much always going to be the cheapest and have the best performance. Doing (2.1) will be a little more expensive, because it requires the use of a Proxy, but not enough that you would normally notice. Doing (2.2) will end up triggering a re-render for every property in the properties used anywhere in the app, even if it didn't change.

    In the case you've described, it appears the fields are well known, which means you should reach for that class. The solution might look something like this:

    import Service from '@ember/service';
    import { action } from '@ember/object';
    import { tracked } from '@glimmer/tracking';
    
    class TheProperties {
      @tracked id;
      @tracked foo;
      @tracked bar;
    }
    
    export default class MyService extends Service {
      properties = new TheProperties();
    
      @action updateProperty(name, value) {
        this.properties[name] = value;
      }
    }
    

    Note that @tracked installs getters and setters in place of plain class properties, so if you need to use this for a JSON payload somewhere or similar, you'll also want to implement toJSON on the utility class:

    class TheProperties {
      @tracked id;
      @tracked foo;
      @tracked bar;
      
      toJSON() {
        let { id, foo, bar } = this;
        return { id, foo, bar };
      }
    }