2

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

  1. 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
  2. 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.
    1. ListView saves the observable response object as a property so it can display a loading or error indicator.
    2. ListView has a computed property that calls Store.getProposals using the same filter object used to fetch
    3. 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:)

Jakke
  • 287
  • 1
  • 2
  • 11
  • I'd be happy to post more code if needed, just trying to shorten the wall of text. – Jakke Sep 07 '16 at 12:07
  • Why is everything prefixed with `@Mobx.` can't you do a destructuring import? – Benjamin Gruenbaum Sep 07 '16 at 12:08
  • 1
    You can just cache the data at the store layer, in a promise or have a Repository class in your MobX to handle touching the server. In general, I've found autorun to indicate a deeper problem with the code. – Benjamin Gruenbaum Sep 07 '16 at 12:09
  • @BenjaminGruenbaum - that's the style we're using so it's obvious what's from Mobx. Meant to take it out in the comment but forgot:/ – Jakke Sep 07 '16 at 12:38
  • @BenjaminGruenbaum - (heh just noticed both comments were you:) I'm caching the data in the store as soon as I get it. I tried to go down the promise road for a bit but didn't have any luck. You wouldn't happen to know of a project that does something like that I could look at as an example? I agree about autorun indicating problems, I'm trying to figure out a better way to structure all of this. Thanks! – Jakke Sep 07 '16 at 12:39
  • I have lots of internal code but nothing external with mobx. I think it would be good for you to significantly reduce the question to something much smaller that's easier to explain. It's still not very clear to me what you're actually asking. – Benjamin Gruenbaum Sep 07 '16 at 13:53

1 Answers1

2

I've been using a different approach in my projects and I extracted it into a separate npm package: https://github.com/mdebbar/mobx-cache

Here's a quick example:

First, we need a React component to display the client info:

@observer
class ClientView extends React.Component {
  render() {
    const entry = clientCache.get(this.props.clientId)

    if (entry.status !== 'success') {
      // Return some kind of loading indicator here.
      return <div>Still loading client...</div>
    }

    const clientInfo = entry.value
    // Now you can render your UI based on clientInfo.
    return (
      <div>
        <h2>{clientInfo.name}</h2>
      </div>
    )
  }
}

Then, we need to setup the clientCache:

import MobxCache from "mobx-cache";

function fetchClient(id) {
  // Use any fetching mechanism you like. Just make sure to return a promise.
}

const clientCache = new MobxCache(fetchClient)

That's all you need to do. MobxCache will automatically call fetchClient(id) when it's needed and will cache the data for you.

Mouad Debbar
  • 2,658
  • 1
  • 18
  • 18
  • Thanks @Jakke! The library is still in early development and I would appreciate any feedback :) – Mouad Debbar Sep 13 '16 at 17:02
  • Interesting. But bad thing is that fetch initiated in `render()` which breaks React idiom to have zero side-effect `render()` – x'ES Sep 06 '18 at 01:18