What is the current idiomatic way to lazy load properties when using MobX?
I've been struggling with this for a few days, and I haven't found any good examples since strict mode has become a thing. I like the idea of strict mode, but I'm starting to think lazy-loading is at odds with it (accessing, or observing a property should trigger the side effect of loading the data if it's not already there).
That's the crux of my question, but to see how I got here keep reading.
The basics of my current setup (without posting a ton of code):
React Component 1 (ListView): componentWillMount
- componentWillMount & componentWillReceiveProps - the component gets filter values from route params (react-router), saves it as an observable object on ListView, and tells the store to fetch 'proposals' based on it
- Store.fetchProposals checks to see if that request has already been made (requests are stored in an ObservableMap, keys by serializing the filter object so two identical filters will return the same response object). It makes the request if it needs to and returns the observable response object that contains info on whether the request is finished or has errors.
- ListView saves the observable response object as a property so it can display a loading or error indicator.
- ListView has a computed property that calls Store.getProposals using the same filter object used to fetch
- Store.getProposals is a transformer that takes a filter object, gets all proposals from an ObservableMap (keys on proposal.id), filters the list using the filter object and returns a Proposal[] (empty if nothing matched the filter, including if no proposals are loaded yet)
This all appears to work well.
The problem is that proposals have properties for client and clientId. Proposal.clientId is a string that's loaded with the proposal. I want to wait until client is actually accessed to tell the store to fetch it from the server (assuming it's not already in the store). In this case ListView happens to display the client name, so it should be loaded shortly after the Proposal is.
My closest I've gotten is setting up a autorun in the Proposal's constructor list this, but part of it is not reacting where I'm indending. (truncated to relevant sections):
@observable private clientId: string = '';
@observable private clientFilter: IClientFilter = null;
@observable client: Client = null;
constructor(sourceJson?: any) {
super(sourceJson);
if (sourceJson) {
this.mapFromJson(sourceJson);
}
//this one works. I'm turning the clientId string into an object for the getClients transformer
autorun(() => { runInAction(() => { this.clientFilter = { id: this.clientId }; }) });
autorun(() => {
runInAction(() => {
if (this.clientId && this.clientFilter) {
const clients = DataStore.getClients(this.clientFilter);
const response = DataStore.fetchClients(this.clientFilter);
if (response.finishedTime !== null && !response.hasErrors) {
this.client = clients[0] || null;
console.log('This is never called, but I should see a client here: %o', DataStore.getClients(this.clientFilter));
}
}
})
});
}
The response object is observable:
export class QueryRequest<T extends PersistentItem | Enum> {
@observable startTime: Date = new Date();
@observable finishedTime: Date = null;
@observable errors: (string | Error)[] = [];
@observable items: T[] = [];
@computed get hasErrors() { return this.errors.length > 0; }
@observable usedCache: boolean = false;
}
I'm getting the feeling I'm fighting the system, and setting up autoruns in the constructor doesn't seem ideal anyway. Anyone solve this pattern in a reasonable way? I'm open to suggestions on the whole thing if my setup looks crazy.
EDIT 1: removed @Mobx for clarity.
EDIT 2: Trying to re-evaluate my situation, I (again) found the excellent lib mobx-utils, which has a lazyObservable function that may suite my needs. Currently it's looking like this:
client = lazyObservable((sink) => {
autorun('lazy fetching client', () => {
if (this.clientFilter && this.clientFilter.id) {
const request = DataStore.fetchClients(this.clientFilter);
if (request.finishedTime !== null && !request.hasErrors) {
sink(request.items[0]);
}
}
})
}, null);
This is working!
I think I need the autorun in there to update based on this objects clientId/clientFilter property (if this object is later assigned to a new client I'd want the lazyObservable to be updated). I don't mind a little boilerplate for lazy properties, but I'm, definitely open to suggestions there.
If this ends up being the way to go I'll also be looking at fromPromise from the same lib instead of my observable request object. Not sure because I'm keeping track of start time to check for staleness. Linking here in case someone else has not come across it:)