20

Let's take a class like this in an app with React and React Router.

@observer class Module1 extends React.Component {

  constructor (props) {
    super(props);
    //...
  }

  componentWillMount(){
    //...
  }

  method(){
    //...
  }

  otherMethod(){
    //...
  }

  render() {
    return (
       <ChildComp bars={this.props.bars}/>}
    );
  }
}

And let's take a state like this

state = observable({
  module1:{
    bars:{
      //...
    }
  },
  module2:{
    foos:{
      //...
    }
  }
})

The Module1 component is loaded like this:

//index.js
render(
      <Router history={browserHistory}>
        <Route path="/" component={App}>
          <Route path='/map' component={Module1} >
            <Route path="/entity/:id" component={SubModule}/>
          </Route>
          <Route path='/map' component={Module2} >
        </Route>
      </Router>,
      document.getElementById('render-target')
    );

How could I pass the props module1.bars to Module1 component? In redux I would use <provider>and redux-connect but I am a bit lost with this in Mobx.js.

dagatsoin
  • 2,337
  • 5
  • 23
  • 48

4 Answers4

43

One week ago we started a new project with with react and mobx, and I faced the same issue as yours. After looking around I found the best way is to use react's context. Here's how:

The store: stores/Auth.js

import { get, post } from 'axios';
import { observable, computed } from 'mobx';
import jwt from 'jsonwebtoken';
import singleton from 'singleton';

import Storage from '../services/Storage';

class Auth extends singleton {
  @observable user = null;
  @computed get isLoggedIn() {
    return !!this.user;
  }

  constructor() {
    super();

    const token = Storage.get('token');

    if (token) {
      this.user = jwt.verify(token, JWT_SECRET);
    }
  }

  login(username, password) {
    return post('/api/auth/login', {
      username, password
    })
    .then((res) => {
      this.user = res.data.user;
      Storage.set('token', res.data.token);
      return res;
    });
  }

  logout() {
    Storage.remove('token');
    return get('/api/auth/logout');
  }
}

export default Auth.get();

Note: we are using singleton to make sure that it's one instance only, because the store can be used outside react components, eg. routes.js

The routes: routes.js

import React from 'react';
import { Route, IndexRoute } from 'react-router';

import App from './App';
import Login from './Login/Login';
import Admin from './Admin/Admin';
import Dashboard from './Admin/views/Dashboard';
import Auth from './stores/Auth'; // note: we can use the same store here..

function authRequired(nextState, replace) {
  if (!Auth.isLoggedIn) {
    replace('/login');
  }
}

export default (
  <Route name="root" path="/" component={App}>
    <Route name="login" path="login" component={Login} />
    <Route name="admin" path="admin" onEnter={authRequired} component={Admin}>
      <IndexRoute name="dashboard" component={Dashboard} />
    </Route>
  </Route>
);

The main component: App.js

// App.js
import React, { Component } from 'react';
import Auth from './stores/Auth';

export default class App extends Component {

  static contextTypes = {
    router: React.PropTypes.object.isRequired
  };

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

  getChildContext() {
    /**
     * Register stores to be passed down to components
     */
    return {
      store: {
        auth: Auth
      }
    };
  }

  componentWillMount() {
    if (!Auth.isLoggedIn) {
      this.context.router.push('/login');
    }
  }

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

And finally, a component using the store: Login.js

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

@observer
export default class Login extends Component {

  static contextTypes = {
    router: React.PropTypes.object.isRequired,
    store: React.PropTypes.object.isRequired
  };

  onSubmit(e) {
    const { auth } = this.context.store; // this is our 'Auth' store, same observable instance used by the `routes.js`

    auth.login(this.refs.username.value, this.refs.password.value)
      .then(() => {
        if (auth.isLoggedIn) this.context.router.push('/admin');
      })
      .catch((err) => {
        console.log(err);
      });

    e.preventDefault();
  }

  render() {
    return (
      <div className="login__form">
        <h2>Login</h2>
        <form onSubmit={this.onSubmit.bind(this)}>
          <input type="text" ref="username" name="username" placeholder="Username" />
          <input type="password" ref="password" name="password" placeholder="Password" />
          <button type="submit">Login</button>
        </form>
      </div>
    );
  }
}

You can declare new stores and add them in getChildContext of App.js, and whenever you need a certain store just declare the store dependency in the component's contextTypes, and get it from this.context.

I noticed that it's not a requirement to pass an observable as prop, just by having the @observer decorator and using any observable value in your component, mobx and mobx-react do their magic.

By the way redux's <Provider store={myStore}><App /></Provider> does the same thing as explained in App.js. https://egghead.io/lessons/javascript-redux-passing-the-store-down-implicitly-via-context

Reference:

thaerlabs
  • 551
  • 1
  • 4
  • 9
  • Great solution thank you! Question: why do you `return !!this.user` ? Is that any different to just doing `return this.user` ? – Barry Michael Doyle Oct 22 '16 at 18:22
  • @BarryMichaelDoyle it's just a shorthand to: `if (this.user) return true; return false;` – thaerlabs Oct 25 '16 at 06:52
  • Oooooh, that's handy. Thanks! – Barry Michael Doyle Oct 25 '16 at 07:36
  • 1
    If you do `const authStore = new Auth(); export default authStore;` then `import authStore from './AuthStore'` you should already have a singleton even if you import the module several times from different modules. Isn't it? Could you clarify this aspect? Is your `singleton` module some npm or standard node module? I am not aware of this – Leonardo Feb 21 '17 at 16:49
  • @Leonardo that's correct, `singleton` is just a utility that I was using until I discovered that CommonJs modules are cached, which means it's not going to run the same `new Auth()` over and over. Good insight, thanks! – thaerlabs Feb 22 '17 at 12:06
  • How would this solution work with the new React 16 context API? – William Chou Jul 17 '18 at 10:24
  • @WilliamChou at this point in time I'd advice using https://github.com/mobxjs/mobx-react. You can use the `Provide` component at the top level of your app, and pass all your store as props, and then use inject, to inject any store you might need in any component. – thaerlabs Jul 20 '18 at 21:27
15

mobx-react offers a (experimental - at the time of writing this) Provider (component) and inject (higher order component) to pass properties to the component hierarchy below.

From above you can use the Provider component to pass all relevant information. Under the hood React context is used.

import { Provider } from 'mobx-react';
...
import oneStore from './stores/oneStore';
import anotherStore from './stores/anotherStore';

const stores = { oneStore, anotherStore };

ReactDOM.render(
  <Provider { ...stores }>
    <Router history={browserHistory}>
      <Route path="/" component={App}>
        <Route path="/" component={SomeComponent} />
      </Route>
    </Router>
  </Provider>,
  document.getElementById('app')
);

In SomeComponent you can retrieve the passed properties by using the inject HOC:

import { observer, inject } from 'mobx-react';
...

const SomeComponent = inject('oneStore', 'anotherStore')(observer(({ oneStore, anotherStore }) => {
  return <div>{oneStore.someProp}{anotherStore.someOtherProp}</div>;
}))

export default SomeComponent;

[Disclaimer: I wrote about it in MobX React: Simplified State Management in React and you can see a minimal boilerplate application which consumes the SoundCloud API.]

Robin Wieruch
  • 10,245
  • 6
  • 68
  • 90
10

First, here is a simple example app that does routing using MobX, React and react-router: https://github.com/contacts-mvc/mobx-react-typescript

In general, personally I like to explicitly pass all the relevant stores as explicit props to my components. But you can also use a package like Ryan to have your stores passed to your components using the React context mechanism, similar to Redux connect (see this app for an example).

Once you have your store in your component, parse the routing params in ComponentWillMount and update your stores accordingly.

That should basically be all :) But let me know if I let anything unanswered.

mweststrate
  • 4,547
  • 1
  • 14
  • 22
  • Does the mobx react typescript example illustrates how to pass them explicitly? – dagatsoin Mar 07 '16 at 18:32
  • Maybe I have a wrong idea on performance impact to pass an entire store in a top component. Maybe the answer at my question is another question: is it good practice to pass an entire store to a component. And the obvious answer is yes. I already do this with redux provider... Do you confirm? – dagatsoin Mar 07 '16 at 18:36
  • 3
    Yes I confirm. A store is just a reference. Should be very efficient to pass around :) It also ensures that you have all the 'model' api's available that you might need in your component. – mweststrate Mar 07 '16 at 19:10
  • Thx for the explanation :) – dagatsoin Mar 07 '16 at 19:23
  • 1
    Is it wrong to import the stores directly in the component rather than passing them through the Router hierarchy? – Robin Wieruch Jul 21 '16 at 19:38
4

Take a look at react-tunnel. It gives you a Provider component and the inject decorator (works like connect in redux).

kuuup
  • 41
  • 1