Mobx: Triggering a fetch within a @computed value

Created on 7 Jun 2016  Â·  27Comments  Â·  Source: mobxjs/mobx

Doing side-effects inside computed values is not encouraged, but it can be useful sometimes:

class BookStore {
  @observable _books = null;
  @computed get books() {
    if (this._books == null) {
      this.fetchBooksFromServer();
    }
    return this._books;
  }

  fetchBooksFromServer() {
    // Make an API request then call `this.booksLoaded` with the response
  }

  @action booksLoaded(response) {
    // Update the list `this._books`
  }
}

export default new BookStore();

The above design makes it simple to only fetch the data that the current UI needs. As soon as a React component references BookStore.books, the store will automatically fetch the list of books from the server. If it's not referenced by any component, books won't be fetched.

I'm curious to know your thoughts about this, and how you design fetching mechanisms such that it only fetches what the UI needs.

Most helpful comment

Yes I actually think this is a really useful pattern indeed, and although it has side effects, I think this has the intended behavior. I have seen other people doing similar things with great success.

In the future I want to add the option to add hooks to be notified when an observable becomes (un)used, so that you could do something like:

@observable({ onBecomeObserved: () => this.fetchBooksFromServer() }) books = null

All 27 comments

Yes I actually think this is a really useful pattern indeed, and although it has side effects, I think this has the intended behavior. I have seen other people doing similar things with great success.

In the future I want to add the option to add hooks to be notified when an observable becomes (un)used, so that you could do something like:

@observable({ onBecomeObserved: () => this.fetchBooksFromServer() }) books = null

@mweststrate re: future ideas like onBecomeObserved, perhaps that's a good idea.. But, just something to keep in mind as mobx evolves and gains adoption is to be careful and picky about any APIs you introduce over time, otherwise it will become bloatware. I don't believe every issue calls for a new feature, it may require a refactor/rethink or just patterns for how to use existing features. Just wanted to remind you about that or you'll run into a bad situation of API bloat.

Without going into a ton of detail I usually do this with a few pieces

  • Define observables/computeds for search criteria or any other parameters for the async request
  • Define an autorun that initiates the async call and on success stores the response in an observable (bonus: the autorun can abort any async call already running)
  • Define computeds on the observable response

Actions that update criteria will transparently cause the any computeds derived from the response to be updated without knowing there was an async call in the middle.

@dave-handy if I understand autorun correctly, it doesn't run when someone starts observing the property. It runs when the observable property changes.

Ah. So I opt for eagerly loading the async, that's true. I guess I don't understand your use case... would waiting for someone to start observation improve your time to render? Would eager loading fetch too many unused things at once?

In single-page-apps, the user can navigate to different pages without refreshing. So we can't just fetch ALL the data on initial render. We need a mechanism that allows us to only fetch data required by the currently rendered components.

Using mobx makes it easy to achieve lazy data fetching. We only fetch data when a component starts observing it.

Most of my async calls require some criteria to be set first (parameters for the lookup) so it certainly doesn't just fetch all data on page load. The UI component sets the criteria, then the fetch is initiated. Also depending on your page, the Store might not be in scope yet. But yes, I see your point.

Ah now I got it. Our use cases are different :)
What if different components need data with different criteria?

That hasn't come up too often yet, but it's easy to instantiate the store class multiple times instead of using it as a singleton.

I only use @computed If I need to compute something. Here is how I do it:

@computed get isEmpty () {
  return !this.books.length
}

fetchBooksIfEmpty () {
  reaction(
    _ => this.isEmpty,
    empty => empty && this.fetchBooks()
  )
}

@flipjs what if the server returns an empty list of books?

Then show a notification that there is nothing to display or server has no data, something like that.

I meant to say that relying on isEmpty is not enough to decide fetching. You need another variable to track the status of the fetch.

I actually dont check for isEmpty. I only pattern it to your example. I use an isInvalidated flag to refresh the data. The code is similar, let me copy it here.

  @action
  fetchAll () {
    this.beforeFetch()
    return this.api.fetchAll()
      .then(action('fetchAll.resolved', data => {
        this.fromApi(data)
        this.afterFetch()
        return Promise.resolve(data)
      }))
      .catch(action('fetchAll.rejected', error => {
        this.fromApiError(error)
        this.afterFetch()
        return Promise.reject(error)
      }))
  }

  @action
  fetchAllAsNeeded () {
    if (!this.isFetching && !this.isEmpty) {
      this.fetchAll()
    }
  }

  @action
  invalidate () {
    this.isInvalidated = true
  }

  @action
  invalidateWatcher () {
    return reaction(
      () => this.isInvalidated,
      isInvalidated => isInvalidated && this.fetchAll()
    )
  }

The @action invalidate can be triggered manually or automatically.

@flipjs I see. So the view needs to call either fetchAllAsNeeded() or invalidate() to trigger the fetch?

I use fetchAllAsNeeded() in my route:

    <Route
      path='users'
      onEnter={loadData(users)}
      component={requiresAuth(UsersView)}
    />
function loadData (store) {
  return () => store.fetchAllAsNeeded()
}

So switching between views, if there is data already, it will not fetch anymore, unless I call invalidate().

That's exactly what I'm trying to avoid :)

I want the mobx store to be smart enough to fetch data based on what's needed. So in my component, all I need to do is access store.books and the store will automatically fetch the books if necessary.

Like I've said, the invalidate() is triggered either manually or automatically. I also have @computed isEmpty. When it becomes true, it will call invalidate(). So the end results is pretty much the same with yours.

I wonder if you could create your own getter outside of the mobx computed
world that would trigger the load as you'd like and return the observable
field?

On Tue, Jun 14, 2016 at 1:43 PM, Felipe Apostol [email protected]
wrote:

Like I've said, the invalidate() is triggered either manually or
automatically. I also have @computed https://github.com/computed isEmpty.
When it becomes true, it will call invalidate(). So the end results is
pretty much the same with yours.

—
You are receiving this because you are subscribed to this thread.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/307#issuecomment-225977502, or mute
the thread
https://github.com/notifications/unsubscribe/AAIrcsuqfQeQ0q4bRf1d6xpCerMFkR-qks5qLvZRgaJpZM4IwVkA
.

-Matt Ruby-
[email protected]

Do you mean object.observe?

Or maybe it can be done with ES6 Proxy

Using :
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get
without mobx for that field. Not sure if you wouldn't run into the same
issues.

Or maybe it can be done with ES6 Proxy
https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Proxy

—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/mobxjs/mobx/issues/307#issuecomment-225981508, or mute
the thread
https://github.com/notifications/unsubscribe/AAIrckyGmRm-1d1AyFrbJGgvhFfR4-3Nks5qLvlogaJpZM4IwVkA
.

@mweststrate I know this is a fairly old issue but do you still stand by your first comment in this thread that putting the fetch in computed is an acceptable approach, or is there a new method that you would recommend?

I just tried implementing this approach and got an error:
[mobx] Invariant failed: Computed values are not allowed to cause side effects by changing observables that are already being observed

I'm new to mobx so it'd be really helpful if someone can provide an example for how to do this the correct way.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

hellectronic picture hellectronic  Â·  3Comments

josvos picture josvos  Â·  3Comments

etinif picture etinif  Â·  3Comments

thepian picture thepian  Â·  3Comments

giacomorebonato picture giacomorebonato  Â·  3Comments