0

I have a MobX data store, called BaseStore that handles the status of an API request, telling the view to render when request is in progress, succeeded, or failed. My BaseStore is defined to be:

class BaseStore {
  /**
   * The base store for rendering the status of an API request, as well as any errors that occur in the process
   */
  constructor() {
    this._requestStatus = RequestStatuses.NOT_STARTED
    this._apiError = new ErrorWrapper()
  }

  // computed values
  get requestStatus() {
    // if there is error message we have failed request
    if (this.apiError.Error) {
      return RequestStatuses.FAILED
    }
    // otherwise, it depends on what _requestStatus is
    return this._requestStatus
  }

  set requestStatus(status) {
    this._requestStatus = status

    // if the request status is NOT a failed request, error should be blank
    if (this._requestStatus !== RequestStatuses.FAILED) {
      this._apiError.Error = ''
    }
  }

  get apiError() {
    // if the request status is FAILED, return the error
    if (this._requestStatus === RequestStatuses.FAILED) {
      return this._apiError
    }
    // otherwise, there is no error
    return new ErrorWrapper()
  }

  set apiError(errorWrapper) {
    // if errorWrapper has an actual Error, we have a failed request
    if (errorWrapper.Error) {
      this._requestStatus = RequestStatuses.FAILED
    }
    // set the error 
    this._apiError = errorWrapper
  }

  // actions
  start = () => {
    this._requestStatus = RequestStatuses.IN_PROGRESS
  }

  succeed = () => {
    this._requestStatus = RequestStatuses.SUCCEEDED
  }

  failWithMessage = (error) => {
    this.apiError.Error = error
  }

  failWithErrorWrapper = (errorWrapper) => {
    this.apiError = errorWrapper
  }

  reset = () => {
    this.requestStatus = RequestStatuses.NOT_STARTED
  }
}

decorate(BaseStore, {
  _requestStatus: observable,
  requestStatus: computed,
  _apiError: observable,
  apiError: computed,
})

That store is to be extended by all stores that consume API layer objects in which all methods return promises. It would look something like this:

class AppStore extends BaseStore { 
    /**
     * @param {APIObject} api
     **/
    constructor(api) { 
        super()
        this.api = api

        // setup some observable variables here
        this.listOfData = []
  this.data = null

        // hit some initial methods of that APIObject, including the ones to get lists of data
        api.loadInitialData
            .then((data) => { 
                // request succeeded

                // set the list of data
                this.listOfData = data
            }, (error) => { 
                // error happened

            })

        // TODO: write autorun/reaction/spy to react to promise.then callbacks being hit


    }

save = () => {
  // clean up the data right before we save it
  this.api.save(this.data)
     .then(() => {
       // successful request

       // change the state of the page, write this.data to this.listOfData somehow
    }, (error) => {
      // some error happened
    })
}

decorate(AppStore, { 
    listOfData : observable,
})

Right now, as it stands, I'd end up having to this.succeed() manually on every Promise resolve callback, and this.failWithMessage(error.responseText) manually on every Promise reject callback, used in the store. That would quickly become a nightmare, especially for non-trivial use cases, and especially now that we have the request status concerns tightly coupled with the data-fetching itself.

Is there a way to have those actions automatically happen on the resolve/reject callbacks?

Mike Warren
  • 3,796
  • 5
  • 47
  • 99
  • You might want to start by [not doing asynchronous stuff inside the constructor](https://stackoverflow.com/questions/24398699/is-it-bad-practice-to-have-a-constructor-function-return-a-promise). – Bergi Aug 06 '19 at 14:28
  • A little beside the point, but should I then move that to a method called `init()` and then invoke it from within the constructor? – Mike Warren Aug 06 '19 at 14:44
  • No, you shouldn't do anything asynchronous from within the constructor, regardless whether it's a method call or not. Put it in a static method that you call *before* the object is instantiated, or if you must in a method that can be called after the constructor has run (the combination of which might be wrapped in a static method). But those allow you to factor everything out into an overwritable method that you can call and await, then do something after it has finished. – Bergi Aug 06 '19 at 15:01
  • Can you show me what that looks like via a non-trivial example? – Mike Warren Aug 06 '19 at 15:12

2 Answers2

0

Make an abstract method that should be overridden by the subclass, and call that from the parent class. Let the method return a promise, and just hook onto that. Don't start the request in the constructor, that only leads to problems.

class BaseStore {
  constructor() {
    this.reset()
  }
  reset() {
    this.requestStatus = RequestStatuses.NOT_STARTED
    this.apiError = new ErrorWrapper()
  }
  load() {
    this.requestStatus = RequestStatuses.IN_PROGRESS
    this._load().then(() => {
      this._requestStatus = RequestStatuses.SUCCEEDED
      this._apiError.error = ''
    }, error => {
      this._requestStatus = RequestStatuses.FAILED
      this._apiError.error = error
    })
  }
  _load() {
    throw new ReferenceError("_load() must be overwritten")
  }
}

class AppStore extends BaseStore {
  constructor(api) { 
    super()
    this.api = api
    this.listOfData = []
  }
  _load() {
    return this.api.loadInitialData().then(data => {
      this.listOfData = data
    })
  }
}

const store = new AppStore(…);
store.load();
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • I thought it was clear, at the time of me writing this question, that I want to automatically set `requestStatus`,`apiError` from **any** API request (basically spying the request Promises themselves/any of their callbacks automatically), but it turns out that I forgot to spell out that it should happen for all of them. I updated the MVCE to provide another such request. – Mike Warren Aug 07 '19 at 01:55
  • @MikeWarren No, you cannot automatically spy on any API request that is somehow triggered in some subclass method, the base class doesn't know about them. You need to explicitly tell it about the request. You can however easily just pass the API request promise to a method for that, like `return this.wrapStatus(this.api.loadInitalData().then(...))` – Bergi Aug 07 '19 at 08:26
0

MobX can update data that is asynchronously resolved. One of the options is to use runInAction function

example code:

    async fetchProjects() {
        this.githubProjects = []
        this.state = "pending"
        try {
            const projects = await fetchGithubProjectsSomehow()
            const filteredProjects = somePreprocessing(projects)
            // after await, modifying state again, needs an actions:
            runInAction(() => {
                this.state = "done"
                this.githubProjects = filteredProjects
            })
        } catch (error) {
            runInAction(() => {
                this.state = "error"
            })
        }
    }

You can read more in official documentation: Writing asynchronous actions

Ivan V.
  • 7,593
  • 2
  • 36
  • 53