Doing this kind of setting-of-tracked-data based on other tracked data is always going to throw that error, for precisely the reason the error message tells you: you can very easily trigger infinite re-rendering that way. The rendering system enforces that you cannot set reactive (tracked) data you have already read from while rendering as a result.
However, in these cases, if you have to (always a question worth asking!) you can sometimes use a combination of untracked local state with smart uses of getters to accomplish the thing you need to: you cache and supply a newly created object in untracked state if it does’t exist. That’s a “fine” approach for e.g. form draft state and similar problems. Here, that approach won’t work without some further tweaking, because when you call this.store.createRecord('note', { house: house });
, you are in fact trying to update tracked state you’ve already read in the same computation: the list of Note
s in the store
!
The problem here, which the rendering layer is telling you, is not that you are computing the data to be displayed (that would be fine!), it’s that your computation of data to be displayed is also creating data!
What I would suggest here is building the list of notes in a single place—the route, if it’s part of the model, or a parent component, if it’s in response to an action, or similar.
While I understand the desire to sort of “encapsulate” this concern in the component here, the requirement that you actually do one-way data flow will often make that more complicated—but in my experience, putting all the data updates together in a single place is much better long-term: it makes it very much easier to understand the data flow throughout the app.[1]
The other thing is, you’re not actually encapsulating this particular data concern by putting it in this component as long as you’re creating a model in the data store. The data store is inherently and inescapably a global concern!
One alternative approach this suggests is to have a piece of data here which is a “draft” of the state you want to create, and which you only turn into a value in the store
once the user persists it. That does allow you to keep the concern local and encapsulated, precisely because it doesn’t couple this to the data store.
In that approach, your component might look something like this:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
class NoteDraft {
house; // can also be tracked if need be
@tracked content; // This should be like the field in your note model!
}
export default class NoteListComponent extends Component {
@service store;
#draft;
#prevHouse;
get draft() {
if (this.args.house !== this.#prevHouse) {
this.#draft = new NoteDraft({ house: this.args.house });
}
return this.#draft;
}
get notes() {
const houseNotes = this.args.house.get('notes');
return houseNotes.some((note) => note.isNew)
? houseNotes
: houseNotes.concat(this.draft);
}
@action saveDraft() {
const note = this.store.createRecord('note', this.#draft);
note.save();
}
}
The key bit to understand here is the draft
getter: we want a new, cached draft any time the house diverges from the previous house
value. This is not tracked, and acts like a cache: any time we get a new house
, we get a new draft
, but it’s otherwise stable. Note that that we never read from tracked state that we’re also assigning to this way! That’s important: it keeps us from having infinite re-rendering cycles. It also will return the same #draft
no matter how times we ask for it, so we get legitimate caching: we’re never accidentally creating a new draft just by asking for the value of the getter this way. It’s also lazy: it will be created on demand when we ask for it in the notes
getter below it, and not otherwise.
The net here is that we have true one-way data flow, no infinite re-rendering concerns. It’s slightly less clean than just having all of this happen at the level where your app notionally “owns” the list of notes, so that would still be my first recommendation—but it works well and maintains the “encapsulation”.
Notes
-
As a bonus: I often think about how I would approach these problems if I were working in a system like Elm or Redux, where the state is all in a single structure—or even in something like Shpadoinkle, where local state transformations have to be pure functions, even though they’re local. For a case like this, you would update the shared state store all together in the same action because you would have to. ⏎