15

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.

m0meni
  • 14,160
  • 14
  • 66
  • 120
twsmith
  • 301
  • 3
  • 14

3 Answers3

20

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.

m0meni
  • 14,160
  • 14
  • 66
  • 120
10

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))

m0meni
  • 14,160
  • 14
  • 66
  • 120
mweststrate
  • 4,547
  • 1
  • 14
  • 22
  • down vote me! your answer is the complete one. will use comments next time for supplementary stuff.. – mweststrate May 31 '16 at 22:20
  • @mweststrate haha I would never down vote. I couldn't answer if you didn't make MobX. – m0meni May 31 '16 at 22:22
  • Would it make sense to recursively apply `action` to functions? Bluebird does something similar with `promisifyAll()` and it's really convenient. At first thought I can't see how it would undermine the intent of `useStrict(true)`. – Jeff Jun 30 '16 at 03:18
4

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;
}
Joel Harkes
  • 9,189
  • 2
  • 41
  • 58
  • Good approach, but `public` and `private` only exists in TS. – VitorLuizC May 01 '19 at 03:36
  • 1
    @VitorLuizC its possible in javascript, but it will get messy (to make private methods). You will need to define both the private method in the constructor method scope and the public method using it must also be created in the constructor itself. – Joel Harkes May 01 '19 at 13:56