Redux boilerplate reduction - laziness or not? Switching to MobX

Ok - this seems to work. Am I using this correctly?

import React from 'react';

import { observable } from 'mobx';
import { observer } from 'mobx-react';

import { CardTitle } from 'material-ui/Card';

import IconButton from 'material-ui/IconButton';
import ActionLockOpen from 'material-ui/svg-icons/action/lock-open';
import ActionLock from 'material-ui/svg-icons/action/lock';

@observer
export class Q6ATitle extends React.Component {
  @observable canEditNew;

  handleClick() {
    this.canEditNew = !this.canEditNew;
    this.props.output(!this.canEditNew);
  }

  renderTitle() {
    if (this.props.lock) {
      return (
        <span>
          {this.props.label}
          <IconButton onClick={this.handleClick.bind(this)}>
            {this.canEditNew ? <ActionLockOpen/> : <ActionLock/>}
          </IconButton>
        </span>
      );
    }

    return this.props.label;
  }

  render() {
    return (
      <CardTitle title={this.renderTitle()}/>
    );
  }
}

Q6ATitle.propTypes = {
  label: React.PropTypes.string.isRequired,
  lock: React.PropTypes.bool.isRequired,
  output: React.PropTypes.func,
};

It should be pointed out that i don’t really understand how to use stores. So in essence I am using Mobx to handle bits of UI state.

(1) I don’t have a constructor of any kind. How would i handle stuff that has to be instantiated on load without the component methods?
(2) Just pass in as props and change my class to a stateless component?
(3) Can someone explain how to use stores (like I’m 5 please)
(4) This seems like a dodgy implementation. It seems to me like i should pass in an observable prop (canEditNew above), and then the Q6ATitle component should just be a stateless component function. Thoughts?

Tat

I do not think it is reasonable to keep an observable within the observer class. You will get the same result just by using the component state functionality. The idea is to split data and the interface, so I operate with the following chain: observable data structures -> observer containers ("smart" components) -> "dumb" components.

Hint: You can use arrow functions for methods definitions, in this case you will not have to to .bind(this).

1 Like

i hear what you are saying, but can’t agree. I know that the rage is all ‘smart’ and ‘dumb’ presentational components which are pure and functional etc. The problem is I am not proficient enough in JS to pull this off. The reason I am using Mobx is precisely to get away from the component state functionality.

Component state functionality handling is a massive pain in the rear, and doing a full redux solution is vast amounts of boilerplate. Mobx just handles it for me… stick an observable on it and it’ll render as it changes. Its like magic.

So i use observables and observers to handle all state within my components. If i need to load something up on first render or sometime in the middle or end etc, the componentWillMount etc lifecycle stuff comes in very handy.

Thanks so much.

Tat

tl,dr: understand what you are saying, but too noob to execute…

Using mobx for component state is more than fine, in fact it is better than setState because it is synchronous.

1 Like

I just wonder why we don’t use createContainer which created by Meteor and maintained by MDG.
I using it and love it.
The problem is we love redux but we can’t use both as well. So let create some thing that use Redux concepts.

That makes sense! However, if I deal with setState from within componentWillReceiveProps I have no side effects, described in the article. Well, setTimeout hack is to be used for some external components, which use the state “wrong” way.

BTW can anyone explain how to put meteor data to mobx store and observe changes?

Regarding load time, remember that duplicate code will be immidiatly gzipped away since IE 6. I am personally a big fan of “boilerplate”, because that makes the code boring and boring is awesome - since its self documenting and contained.

1 Like

It is simple as… Use a container to load up the data from the reactive source. Then, pass same into component as a prop.

Once it is a prop, in componentWillMount or something similar (componentWillReact is the mobx specific one), set an @observable = this.props.whatever. As long as your class has an @observer on it, so…

import React from 'react';

import { observable } from 'mobx';
import { observer } from 'mobx-react';


@observer
export class <someClass> extends React.Component {
  @observable <someObservable>;

  componentWillReact() {
    this.<someObservable> = this.props.<propFromContainer>;
  }

  render() {
    return (<span>{this.<someObservable>}</span>
  }
}

<Proptypes>

I’m using it mostly to manage random bits of state. However, for stores, my understanding is that you pass mobx specific proptypes: observableArray, observableMap, observableObject, arrayOrObservableArray, objectOrObservableObject

1 Like

I do it like this:

/client/imports/lib/state.js
=============================

import { reactionTracker, syncWithCollection } from "/client/imports/lib/state-helpers"

class UiState {
    @observable lang = ""
    @observable tags = []

    constructor() {
        this.setLanguage( "en" )

        Meteor.subscribe( "tags" )
        syncWithCollection( this, "tags", Tags, {
            transformer: data => data.map( tag => ({
                color: defaultTagColor,
                ...tag,
                enabled: false,
                toggle: action( "TAG_TOGGLE", () => this.tagToggle( tag.name ) ),
            }) ),
        } ).start()
    }

   setLanguage( lang ) {
        if( !lang ) return
        TAPi18n.setLanguage( lang )
        const self = this
        const _set =  action("LANG_SET", () => { self.lang = lang } )
        const _checkAndSet =  () => {
            if( TAPi18next.options.resStore[ lang ] ) _set()
            else Meteor.setTimeout( _checkAndSet, 500 )
        }
        _checkAndSet()
    }

    tagToggle( name ) {
        const tag = this.tags.filter( tag => tag.name === name )[0]
        if( tag ) tag.enabled = !tag.enabled
    }
}

const uiState = new UiState()
export default uiState
/client/imports/lib/state-helpers.js
====================================

import { Tracker } from "meteor/tracker"
import { action, observable, useStrict } from "mobx"

/* Disallow state updates out of the actions */
useStrict( true )

export function syncWithCollection( object, property, collection, { filter = {}, options = {}, transformer, log = false } = {} ) {
    if( !object || !property || !collection ) return
    const syncAction = action( property.toUpperCase() + "_SYNC", () => {
        if( log ) console.log( "syncWithCollection/sync | property:", property )
        const cursor = collection.find( filter, options )
        let result = cursor.fetch()
        if( typeof transformer === "function" ) result = transformer(result)
        if( log ) console.log( "syncWithCollection/sync | property:", property, ", count:", cursor.count(), "result:", result )
        object[ property ] = result
    })
    let computation
    let started = false
    if( log ) console.log( "syncWithCollection/init | property:", property )
    return {
        start() {
            if( started ) return false
            if( log ) console.log( "syncWithCollection/start | property:", property )
            computation = reactionTracker( syncAction, log )
            computation.start()
            started = true
            return true
        },
        stop() {
            if( !started ) return false
            if( log ) console.log( "syncWithCollection/stop | property:", property )
            computation.stop()
            started = false
            return true
        },
    }
}

export function reactionTracker( reaction, log ) {
    let computation = null
    let started = false
    if( log ) console.log( "reactionTracker/init" )
    return {
        start() {
            if( started ) return false
            if( log ) console.log( "reactionTracker/start" )
            computation = Tracker.autorun( () => {
                if( log ) console.log( "reactionTracker/computation" )
                reaction()
            })
            started = true
        },
        stop() {
            if( !started ) return false
            if( log ) console.log( "reactionTracker/stop | started:", started )
            computation.stop()
            started = false
        }
    }
}
1 Like

@tastemeru

BTW can anyone explain how to put meteor data to mobx store and observe changes?

tracker-mobx-autorun package was released couple of days ago to solve exactly that problem:

I just had discussion about it here:

Please check it out and give us feedback if you decide to use it.

1 Like

@darkomijic thanks for the package, I’m observing it since the first commits and already gave it a short try, but still have a few questions:

  1. I see that you suggest subscribe to the meteor data in Meteor.startup, but what to do with page specific data? Right I understand, that in this case we should use react container with componentWillMount/unmount to start and stop autorun?
  2. When I gave it a short shot, I passed a store state from top to bottom component and every component was wrapped in mobx @observer, but how I saw in mobx dev tool, when I added new item or delete it from meteor collection, every single component fired as updated in mobx dev tool(I guess I did smth wrong). Right I understand that tracker-mobx-autorun and mobx should handle this fires and update only necessary components when I insert or delete from mongo collection(kinda handle all “shouldComponentUpdate” job for me?)

I see that you suggest subscribe to the meteor data in Meteor.startup, but what to do with page specific data? Right I understand, that in this case we should use react container with componentWillMount/unmount to start and stop autorun?

React lifecycle methods would work nicely to start and stop autoruns. It is the same concept as template based subscriptions. I am taking a different approach, my autoruns are running constantly and I am managing data loading with routing.

This is from work in progress project, I am porting Mantra sample blog to this ui stack:

 FlowRouter.route('/post/:postId', {
      name: 'posts.single',
      action({postId}) {
        mount(MainLayout, {
          content: () => (<Post postId={postId}/>)
        });
      },
      triggersEnter: [
        selectPostTrigger
      ],
      triggersExit: [
        deselectPostTrigger
      ]
    });

Action:

import state from '../store';
import { action } from 'mobx';

export const selectPost = action('selectPost', postId => {
  state.selectedPostId = postId;
});

export const deselectPost = action('deselectPost', () => {
  state.selectedPostId = null;
});

And my still not completed autorun looks like this:

import state from '../store';
    import * as Collections from '../../lib/collections';
    import { Meteor } from 'meteor/meteor';
    import { action } from 'mobx';

    export default () => {

      // SELECTED POST
      const postId = state.selectedPostId;
      const options = {
        sort: {createdAt: -1}
      };

      let commentsSubscriptionHandle;

      // TODO: manage loading state if subscription is not ready

      if (postId) {
        commentsSubscriptionHandle = Meteor.subscribe('posts.comments', postId);
      } else {
        commentsSubscriptionHandle && commentsSubscriptionHandle.stop();
      }
      action('updatePostFromAutorun', (comments) => {
        state.comments = comments;
      })(postId && commentsSubscriptionHandle.ready() ? Collections.Comments.find({postId}, options).fetch() : []);

    };

MobX is running in strict mode in above example and comments for post are loaded if post is selected, there is no need for starting and stopping comments autorun.

When I gave it a short shot, I passed a store state from top to bottom component and every component was wrapped in mobx @observer, but how I saw in mobx dev tool, when I added new item or delete it from meteor collection, every single component fired as updated in mobx dev tool(I guess I did smth wrong). Right I understand that tracker-mobx-autorun and mobx should handle this fires and update only necessary components when I insert or delete from mongo collection(kinda handle all “shouldComponentUpdate” job for me?)

This should not be happing, is the data provided to components MobX observable? Can you share the code for me to look at it? You should also be careful with structuring autoruns, keep them small and focused to specific part of state. They will rerun if any piece of MobX managed state changes or Tracker-aware data source updates (in this case Minimongo).

Nice idea! I think you could add this info somewhere in the package FAQ

My example app.
Store:

export default observable({
  projects: [],
});

Autorun:

export default () => {
  Meteor.subscribe( 'Projects.Private' )
  store.projects = Projects.find({}, {sort: {createdAt: -1}}).fetch();
};

High level component:

import store from '../store/index.jsx';

@observer export default class ProjectsListPageTest extends React.Component {
  constructor(props) {
    super(props);
  }

  componentWillMount() {
      const projectsAutorun = autorun(projects);
      projectsAutorun.start();
  }

  render() {
    return (
      <div>
	<ProjectCard projects={store.projects} />
      </div>
    );
  }
}

ProjectCard:

const ProjectCard = observer(({projects}) => {
  return (
    <div className="row projects-container">
      {projects.map(project => (
          <Card project={project} key={project._id} />
        ))
      }
    </div>
  );
});

Card:

@observer export default class Card extends React.Component {
  static propTypes = {
    name: React.PropTypes.string,
  };

  constructor(props) {
    super(props);
  }
  // shouldComponentUpdate(nextProps, nextState) {
  //   return nextProps.project.title !== this.props.project.title;
  // }
  render() {
      const {project} = this.props;
      console.log('Card', project._id);
      return (
        <div className="project-card">
          {project.title}
        </div>
      );
  }
}

In this example when I’m adding new items to a collection, then it fires console.log('Card', project._id) for all current cards and if I uncomment shouldComponentUpdate, then it fires only once for a new Card.

Be careful with this. You are creating new autorun every time component mounts. You should keep reference to your autorun instance and have only one per application instance.

Here is a snippet from documentation. In your case you can instantiate and export autruns in one place and than import them in your components and start them there.

// index.js
import autorun from 'meteor/space:tracker-mobx-autorun';
import todos from './autorun/todos';

export const todosAutorun = autorun(todos);

:+1:
Thanks, but it still fires rerenders for all components, any ideas why?

You are setting store.projects to an empty array and than to fetched data since you are not waiting for subscription to be ready, change your autorun to this:

export default () => {
  const handle = Meteor.subscribe( 'Projects.Private' );
  if (handle.ready()) {
    store.projects = Projects.find({}, {sort: {createdAt: -1}}).fetch();
  }
};

Let me know if this two things fixed the issue, I will look again if not. :slight_smile:

I tried to implement this to a react todos example app from meteor tutorial and it seems that mobx-tracker re-renders whole todo list after adding new one.

Here is an example repo https://github.com/tastemeru/test-react-mobx-meteor

@darkomijic could you show on the example react-todos app, how to use tracker-mobx-autorun ?

Sure, I will do that later today.

@tastemeru:

There is one more thing that is causing re-rendering, in autoruns you need to keep instance of observable array, code that you are currently using replaces one instance of observable array with new one when fetched array is assigned.

Here is how autorun should look:

export default () => {
  Meteor.subscribe( 'Projects.Private' );
  store.projects.replace(Projects.find({}, {sort: {createdAt: -1}}).fetch());
};

Check out MobX documentation for replace in observable arrays:
https://mobxjs.github.io/mobx/refguide/array.html

1 Like