MigoFast MigoFast - 4 months ago 63
React JSX Question

mobx with react store not reflected in observer

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>

Answer

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