1

As with all things react I'm trying to do something simple and I'm guessing I'm missing some obvious configuration wise all I'm trying to do is take a redux app and implement mobx.

My issue is that I trying to go to the route /user/12345

The store is being called - I am getting data back from the API but I'm getting a few exceptions the first is from mobx

An uncaught exception occurred while calculation your computed value, autorun or tranformer. Or inside the render().... In 'User#.render()'

Then as is somewhat expected a null value is blowing up in the presenter because the store is not yet loaded

 Uncaught TypeError: Cannot read property 'name' of null

Then in the store itself because of the returned api/promise where my user is being set a mobx exception

Uncaught(in promise) Error: [mobx] Invariant failed: It is not allowed to create or change state outside an `action`

I have added @action to the store function that is calling the api so I'm not sure what I've got messed up - and before I bubble gum and hodge podge a fix I would rather have some feedback how to do this correctly. Thanks.

UserStore.js

import userApi from '../api/userApi';
import {action, observable} from 'mobx';

class UserStore{
   @observable User = null;

   @action getUser(userId){
      userApi.getUser(userId).then((data) => {
         this.User = data;
      });
   }
}

const userStore = new UserStore();
export default userStore;
export{UserStore};

index.js

import {Router, browserHistory} from 'react-router';
import {useStrict} from 'mobx';
import routes from './routes-mob';

useStrict(true);
ReactDOM.render(
    <Router history={browserHistory} routes={routes}></Router>,
    document.getElementById('app')
);

routes-mob.js

import React from 'react';
import {Route,IndexRoute} from 'react-router';
import App from './components/MobApp';
import UserDetail from './components/userdetail';

export default(
    <Route name="root" path="/" component={App}>
       <Route name="user" path="user/:id" component={UserDetail} />
    </Route>
);

MobApp.js

import React,{ Component } from 'react';
import UserStore from '../mob-stores/UserStore';

export default class App extends Component{
   static contextTypes = {
      router: React.PropTypes.object.isRequired
   };

   static childContextTypes = {
      store: React.PropTypes.object
   };

   getChildContext(){
      return {
         store: {
            user: UserStore
         }
      }
   }

   render(){
      return this.props.children;
   }

}

.component/userdetail/index.js (container?)

import React, {Component} from 'react';
import userStore from '../../mob-stores/UserStore';
import User from './presenter';

class UserContainer extends Component{
   static childContextTypes = {
       store: React.PropTypes.object
   };

   getChildContext(){
      return {store: {user: userStore}}
   }

   componentWillMount(){
      if(this.props.params.id){
         userStore.getUser(this.props.params.id);
      }
   }

   render(){
      return(<User />)
   }
}

export default UserContainer;

.component/userdetail/presenter.js

import React, {Component} from 'react';
import {observer} from 'mobx-react';

@observer
class User extends Component{
   static contextTypes = {
      store: React.PropTypes.object.isRequired
   }

   render(){
      const {user} = this.context.store;
      return(
          <div>{user.User.name}</div>
      )
   }
}

Forgive me if its a little messy its what I've pieced together for how to implement mobx from various blog posts and the documentation and stack overflow questions. I've had a hard time finding a soup-to-nuts example that is not just the standard todoapp

UPDATE

Basically the fix is a combination of @mweststrate answer below adding the action to the promise response

@action getUser(userId){
  userApi.getUser(userId).then(action("optional name", (data) => {
     // not in action
     this.User = data;
  }));

}

and including a check in the presenter that we actually have something to display

<div>{this.context.store.user.User ? this.context.store.user.User.name : 'nada' }</div>
Danila
  • 3,703
  • 2
  • 11
  • 32
maehue
  • 495
  • 8
  • 25
  • The first error is thrown because of the second one (missing `name`), but I've only run against the third one when I (mistakenly) modified an observable inside a `render` (which is wrapped by `@observable`), which is not what your code is doing. – robertklep Jul 29 '16 at 19:05
  • Am I doing the context correctly? – maehue Jul 29 '16 at 19:12
  • tbh, I use [`mobx-connect`](https://github.com/nightwolfz/mobx-connect) for that so I can't say for sure. But it should be easily testable by initialing the `User` observable with an object and commenting out the code that fetches the user data. – robertklep Jul 29 '16 at 19:16

1 Answers1

4

Note that the following code is not protected by an action:

   @action getUser(userId){
      userApi.getUser(userId).then((data) => {
         // not in action
         this.User = data;
      });
   }

The action only decorates the current function, but the callback is a nother function, so instead use:

   @action getUser(userId){
      userApi.getUser(userId).then(action("optional name", (data) => {
         // not in action
         this.User = data;
      }));
   }
mweststrate
  • 4,547
  • 1
  • 14
  • 22
  • I tried that and it is the same - is it that I should be checking for a/the value inside of .component/userdetail/presenter.js before I attempt to use it? I can only get it to work if I change my presenters render method to
    {this.context.store.user.User ? this.context.store.user.User.name : 'nada' }
    - this initially flashes 'nada' and then displays the correct value after the store is updated (the api call completes)
    – maehue Aug 01 '16 at 16:15
  • That sounds like correct behavior, your promise will take an undefined amount of time to complete, so you should define your UI in the absence of an incompleted promise as well. Otherwise your UI would need to "freeze" until your promise satisfies, a scenario that neither MobX nor React wants to encourage :) – mweststrate Aug 01 '16 at 18:02