How to handle POJO's, regarding tracking, properly?

Please consider following use-case and please let me know your thoughts :grinning:

//routes/user-edit.js
export default class EditUserRoute extends Route {
  model() {
    // i.e. GraphQL Request, which returns a POJO
    return {
      firstName: 'Michael',
      lastName: 'Knight',
      address: {
        street: 'Schoolstreet 22',
        town: 'New York'
      }
    };
  }
}
//templates/user-edit.js
<UserEdit @user={{this.model}} />
//components/user-edit.js
export default class UserEditComponent extends Component {
  @tracked user = null; // I think this tracked is for nothing, isn't it?

  constructor() {
    super(...arguments);
    this.user = this.args.user;
  }

  @action updateFirstName({ target }) {
     this.user.firstName = target.value; // I know, that {{user.firstName}} is not rerendered if this action is triggered. I just want to know, what's your recommended solution to this issue. Please see below.
   }
}
{{!-- ... --}}
<input placeholder="Firstname" type="text" value={{this.user.firstName}} {{on "input" this.updateFirstName}}>
{{!-- ... --}}
<input placeholder="Street" type="text" value={{this.user.address.street}} {{on "input" this.updateStreet}}>
{{!-- ... --}}
{{user.firstName}}
<br>
{{user.lastName}}
<br>
{{user.address.street}}

Solutions:

  1. Convert the POJO from the model hook into a native Class.
class Address {
  @tracked street;
  @tracked town;

  constructor({ street, town }) {
    this.street = street;
    this.town = town;
  }
}
class User {
  @tracked firstName;
  @tracked lastName;

  constructor({ firstName, lastName, address }) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.address = new Address(address);
  }
}
//routes/user-edit.js
export default class EditUserRoute extends Route {
  model() {
    // i.e. GraphQL Request, which returns a POJO
    return new User({
      firstName: 'Michael',
      lastName: 'Knight',
      address: {
        street: 'Schoolstreet 22',
        town: 'New York'
      }
    });
  }
}
  1. Keep the POJO and use { set } from '@ember/object'
  @action updateFirstName({ target }) { 
     set(this.user, 'firstName', target.value);
  }

For me 1. seems a bit costly. But what is better for the long run? Thanks

I would either:

  • use TrackedObject from https://github.com/pzuraq/tracked-built-ins

  • or use a more functional style that avoids mutation:

    @action updateFirstName({ target }) {
      this.user = { firstName: target.value, ...this.user};
    }
    

Also, yes, this.user = this.args.user doesn’t appear to be doing anything useful in this example. You can use this.args.user directly, it’s already tracked, and from the template you can access it like {{@user.firstName}} But if you do that, you can’t replace it like my second option above.

3 Likes

@ef4 Thank you. I like your second option, the functional approach. It seems more native and reminds me of React.js :grinning:.

best regards

@wuarmin FWIW, TrackedObject is also very much native. It uses a built in feature in JavaScript, Proxy, to trap and track access to the object. Combined with Ember’s backtracking assertion that prevents mutation after usage, you get very similar guarantees to the functional approach.

If you do still want to use the functional approach, I recommend taking a look at Immer.js, it makes a lot of edge cases for that style much easier and fits nicely with autotracking.

3 Likes

Thanks @pzuraq. I appreciate your input. Immer.js seems to be a very helpful and great tool at functional programming. I also starred the tracked-built-in addon. I will try both options. I think it’s a matter of taste, whereat, I think your approach (tracked-built-in) matches better with ember.js.