Is there a way to avoid using createContainer?


#1

I have used the createContainer method for setting up container components that collect data from the Meteor stack. Is there a way to simply use a standard React component as the container to collect data from Mongo, rather than use createContainer?


#2

Why don’t you want to use createContainer? It is probably you’re best option if you want “Tracker Magic.” If you want to setup you’re own version of minimongo you could look into mobx or redux to manage a data store client side. Just know that you’ll have to write A LOT of boilerplate.


#3

Isn’t this the standard way to bind data to React? That’s how Redux, etc do it as well.

Your other option is to read the source code for createContainer and re-implement it yourself.


#4

React Komposer, Tracker React, and Tracker Component are all options for getting Tracker to work with React. I think Tracker React and Tracker Component let you have more fine grained control over the reactivity. React Komposer and createContainer encourage you to wrap all your reactivity into one function…


#5

In short, I wanted to use React without having to rely on Meteor specific containers. I found an article that someone had written that describes how to get data from Mongo and feed it into Redux:

It was starting to do my head in a little and also seemed that placing data from MiniMongo into a Redux store was an unecessary step (because we could simply use the createContainer() to pump data into the receiving component).

At the moment I am new to this technique and am hoping that it might remove the need to use callback functions as props, especially when they are nedded in components two or more levels deep. Would be good to here other peoples take on this.


#6

If you take Meteor out, then aren’t you just building a React app? The point of the Meteor containers is to feed Meteor-specific data into your React components.

Redux is overkill in most cases, I’ve come to learn.

The pattern I use is this:

  • I try to keep as many React components as pure as possible, meaning no Meteor-specific “stuff” used in them.
  • When it’s necessary to access Mongo data or some other kind of Meteor-specific data construct, I’ll implement a container, so for instance: RecordListContainer is the Meteor container, which wraps RecordList which should have (ideally) nothing Meteor-related in it.
  • RecordListContainer will import Meteor validated methods and pass them as props into RecordList so that they can be called, e.g. this.props.someMethod.call(...). Or if you wanted to go even purer, you could do what I usually do:

(in the container)

const someMethod = (args, callback) => {
  _someMethod.call(args, callback);
};

And then pass someMethod into RecordList (so you won’t have to use .call() which is slightly impure).

The thinking behind this is that if you decide some day that you want to rip out Meteor, you could do so and it wouldn’t require you to spend several days refactoring all your components to take out the Meteor-specific stuff. You’d only have to touch the containers.


#7

@ffxsam - mate, its a big ask, but when you get some time can you put together a small gist with this implemented. I’d love to see how you implement this legitly.

I do something similar myself, and use react-komposer (themeteorchef style) to inject reactive arrays into the react components. The downside is, the method to make the meteor call, has to be sent down to the lowest container which can make a change. So in practise, a mobX store + a function to change it around (large switch block) is what gets sent to every container.

I’d love to see how someone else does this - from memory you use mobx pretty heavily too?

Thanks so much.

Tat


#8

Sure! You talking about a fully working Meteor app? Or just a couple JS files showing the full code?


#9

A couple of files should be fine. My setup is react-router loads the container. So looking to see what a the container looks like, and the component it injects into basically.

If you could add a subcomponent which needs to manipulate info in the higher component (via a setState or mobX reaction), would also be most appreciated. Another option would be to have the component change something in the publication in the container which should show the same pattern (if I understand what you have stated above correctly)

If I’ve been too specific with my requirement above, feel free to ignore (i.e. tell me to f off) and just explain in words. It is super appreciated and I don’t want to impose.

Tat

p.s. to better explain where I am coming from, my containers look like (using react-komposer):

import { Meteor } from 'meteor/meteor';
import { composeAll, composeWithTracker } from 'react-komposer';

// spinning round thing
import { Loading } from '../components/utility/loading.js';

// Entities simpleSchema
import { Entities } from '../../api/entities/entities.js';

// Inject into here...
import { Admin } from '../components/admin/admin.js';

const composerEntities = (params, onData) => {
  const subscription = Meteor.subscribe('admin.entities');
  if (subscription.ready()) {
    const entities = Entities.find().fetch();
    onData(null, { entities });
  }
};

const composerUsers = (params, onData) => {
  const subscription = Meteor.subscribe('admin.users');
  if (subscription.ready()) {
    const users = Meteor.users.find().fetch();
    onData(null, { users });
  }
};

export default composeAll(
  composeWithTracker(composerEntities, Loading, null, { pure: false }),
  composeWithTracker(composerUsers, Loading, null, { pure: false }),
)(Admin);

This leads to admin.js, which has, right at the bottom, props which i use wherever or pass to sub components as needed…

Admin.propTypes = {
  entities: PropTypes.array.isRequired,
  users: PropTypes.array.isRequired,
};

#10

Here’s a couple files from a project I’m working on: (some of this is quite lengthy so I’m trimming it and just leaving the relevant parts)

SignupFormContainer.js

import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';

import SignupForm from './SignupForm';
import { createAccount as _createAccount } from '../../methods';

const createAccount = (args, callback) => {
  _createAccount.call(args, callback);
};

export default createContainer(() => {
  return {
    createAccount,
    sitekey: Meteor.settings.public.recaptchaSiteKey,
  }
}, SignupForm)

SignupForm.js

import React, { Component, PropTypes } from 'react';
import Radium from 'radium';
import {
  RaisedButton,
  TextField,
} from 'material-ui';
import Formous from 'formous';
import validator from 'validator';

// This is actually not ideal as it's a Meteor package - should switch to React i18n
import { _i18n as i18n } from 'meteor/universe:i18n';

import ReCAPTCHA from '../ReCAPTCHA';
import WarningConfirmation from
  '/imports/common-ui/components/WarningConfirmation';
import DelayedSpinner from '/imports/common-ui/components/DelayedSpinner';

class SignupForm extends Component {
  constructor(props) {
    super(props);
    this.state = {
      busy: false,
      isHuman: true,
      signupErrorMessage: '',
    };
  }

  handleSubmit = (formStatus, fields) => {
    if (!formStatus.touched) {
      this.setState({ signupErrorMessage: i18n.__('form.untouched') });
      return;
    }

    if (!formStatus.valid) {
      if (fields.password.value !== fields.passwordConfirm.value) {
        this.setState({ signupErrorMessage: i18n.__('form.passwordMismatch') });
      }

      return;
    }

    const {
      betaCode,
      email,
      password,
    } = fields;

    this.setState({ busy: true });

    this.props.createAccount({
      betaCode: betaCode.value,
      email: email.value,
      password: password.value,
    }, (error, result) => {
      this.setState({ busy: false });

      if (!error) {
        this.props.onSignup(result);
      } else {
        if (/Email already exists/.test(error.reason)) {
          this.setState({
            signupErrorMessage: 'An account with that email address already ' +
            'exists.',
          });
        } else {
          this.setState({
            signupErrorMessage: error.reason,
          });
        }
      }
    });
  };

  renderCaptcha = () => {
    return <ReCAPTCHA
      siteKey={this.props.sitekey}
      onVerificationExpired={() => this.setState({ isHuman: false })}
      onVerifiedHuman={() => this.setState({ isHuman: true })}
    />
  };

  render() {
    const {
      fields: {
        betaCode,
        email,
        password,
        passwordConfirm,
      },
      formSubmit,
    } = this.props;

    return <div style={styles.root}>
      <div style={styles.form}>
        <form onSubmit={formSubmit(this.handleSubmit)}>
          <div>
            <TextField
              id="email"
              floatingLabelText="Email"
              { ...email.events }
              { ...email.failProps }
            />
          </div>
          <div>
            <TextField
              id="password"
              floatingLabelText="Password"
              type="password"
              { ...password.events }
              { ...password.failProps }
            />
          </div>
          <div>
            <TextField
              id="passwordConfirm"
              floatingLabelText="Confirm Password"
              type="password"
              { ...passwordConfirm.events }
              { ...passwordConfirm.failProps }
            />
          </div>
          <div>
            <TextField
              id="betaCode"
              floatingLabelText="Beta Code"
              { ...betaCode.events }
              { ...betaCode.failProps }
            />
          </div>

          <div style={styles.captcha}>
            {this.renderCaptcha()}
          </div>

          <div style={styles.signupButtonContainer}>
            <RaisedButton
              style={styles.signupButton}
              disabled={!this.state.isHuman}
              label="Sign Up"
              primary
              type="submit" />
          </div>
        </form>
      </div>

      <WarningConfirmation
        open={!!this.state.signupErrorMessage}
        type="ok"
        onConfirm={() => this.setState({ signupErrorMessage: '' })}
      >
        {this.state.signupErrorMessage}
      </WarningConfirmation>

      {this.state.busy ? <DelayedSpinner overlay size={1} wait={200} /> : null}
    </div>
  }
}

const styles = {
// snipped
};

const formousOptions = {
// snipped
};

export default Radium(Formous(formousOptions)(SignupForm))

#11

If you don’t need the reactivity of a container, you could always fetch data from Mongo using Meteor methods in the component’s lifecycle methods.


#12

This is exactly where I am - thinking about dropping Meteor out of the application, so I am addressing my existing containers (which use createContainer) and trying to implement a more React-centric style. Long-term goal will be to use some sort of GraphQL server.


#13

Do you have a short example to help visualize how to do this?


#14

thank you sir - very much appreciated…


#15

Don’t really have any example but here’s the idea to get you started, you would put something like this in a container component:

componentDidMount(){
   Meteor.call('fetchData', (error, data) => {
       if(error){ 
          console.log(error);
      } else {
       this.setState({fetchedData : data});
      }
   });
}

#16

This’ll work too. And then you could just name this component MyWhateverContainer so that you can easily tell at a glance that it’s not pure React.


#17

Thanks for the example. Do you have an example which allows for the data to be reactive, i.e. when changes to the data are made it automatically updates the UI?


#18

That’s not possible without using createContainer.


#19

It is not true (-:
You can use Apollo with polling or subscription.


#20

Ahh… yes, I’m not up to date with Apollo at all. My mistake :slight_smile: