15
votes

I'm using MobX 2.2.2 to try to mutate state inside an async action. I have MobX's useStrict set to true.

@action someAsyncFunction(args) {
  fetch(`http://localhost:8080/some_url`, {
    method: 'POST',
    body: {
      args
    }
  })
  .then(res => res.json())
  .then(json => this.someStateProperty = json)
  .catch(error => {
    throw new Error(error)
  });
}

I get:

Error: Error: [mobx] Invariant failed: It is not allowed to create or change state outside an `action` when MobX is in strict mode. Wrap the current method in `action` if this state change is intended

Do I need to supply the @action decorator to the second .then statement? Any help would be appreciated.

3

3 Answers

20
votes

Do I need to supply the @action decorator to the second .then statement? Any help would be appreciated.

This is pretty close to the actual solution.

.then(json => this.someStateProperty = json)

should be

.then(action(json => this.someStateProperty = json))

Keep in mind action can be called in many ways that aren't exclusive to @action. From the docs on action:

  • action(fn)
  • action(name, fn)
  • @action classMethod
  • @action(name) classMethod
  • @action boundClassMethod = (args) => { body }
  • @action(name) boundClassMethod = (args) => { body }

are all valid ways to mark a function as an action.

Here's a bin demonstrating the solution: http://jsbin.com/peyayiwowu/1/edit?js,output

mobx.useStrict(true);
const x = mobx.observable(1);

// Do async stuff
function asyncStuff() {
  fetch('http://jsonplaceholder.typicode.com/posts')
    .then((response) => response.json())
    // .then((objects) => x.set(objects[0])) BREAKS
    .then(mobx.action((objects) => x.set(objects[0])))
}

asyncStuff()

As for why your error actually happens I'm guessing that the top level @action doesn't recursively decorate any functions as actions inside the function it's decorating, meaning your anonymous function passed into your promise wasn't really an action.

10
votes

To complement the above answer; indeed, action only works on the function you pass to it. The functions in the then are run on a separate stack and should therefor be recognizable as separate actions.

Note that you can also give the actions a name as well so that you easily recognize them in the devtools if you use those:

then(action("update objects after fetch", json => this.someStateProperty = json))

4
votes

note that in async method you manualy have to start a new action/transaction after awaiting something:

@mobx.action async someAsyncFunction(args) {
  this.loading = true;

  var result = await fetch(`http://localhost:8080/some_url`, {
    method: 'POST',
    body: {
      args
    }
  });
  var json = await result.json();
  @mobx.runInAction(()=> {
     this.someStateProperty = json
     this.loading = false;
  });
}

my preference

I prefer to not use @mobx.action/runInAction directly but always place it on an private method. And let public methods call private methods that actually update the state:

public someAsyncFunction(args) {
  this.startLoading();
  return fetch(`http://localhost:8080/some_url`, {
    method: 'POST',
    body: {
      args
    }
  })
  .then(res => res.json())
  .then(this.onFetchResult);
}

@mobx.action 
private startLoading = () => {
   this.loading = true;
}

@mobx.action 
private onFetchResult = (json) => {
   this.someStateProperty = json;
   this.loading = false;
}