Redux boilerplate reduction - laziness or not? Switching to MobX

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