Keeping React components pure in a Meteor app


#1

Somewhere along the road of JavaScript fatigue, I believe I had developed a bad habit. I wanted to keep my React components free of any Meteor-related stuff, so I was relying on Redux as purely an action dispatcher:

class MyComponent extends Component {
  handleSubmit(args) {
    this.props.dispatch(updateUserProfile(args));
  }
}

So then I’d have a whole bunch of Redux actions that were basically just thunks, and dispatched another action to report whether the Meteor method had succeeded or failed:

export const UPDATE_USER_PROFILE = 'UPDATE_USER_PROFILE';
export function updateUserProfile(args) {
  return (dispatch) => {
    updateProfile.call(args, (error) => {
      if (!error) dispatch(updateUserProfileSucceeded());
      else dispatch(updateUserProfileFailed(error.reason));
    });
  }
}

export const UPDATE_USER_PROFILE_SUCCEEDED = 'UPDATE_USER_PROFILE_SUCCEEDED';
export function updateUserProfileSucceeded() {
  return {
    type: UPDATE_USER_PROFILE_SUCCEEDED,
  }
}

export const UPDATE_USER_PROFILE_FAILED = 'UPDATE_USER_PROFILE_FAILED';
export function updateUserProfileFailed(errorMessage) {
  return {
    type: UPDATE_USER_PROFILE_FAILED,
    errorMessage, // but probably not best suited for a persistent state model
  }
}

Looking back now, I don’t think this is a great way to keep components pure. So now I’m experimenting with another approach that I wanted to share, just in case it helps anyone, or if anyone who’s been down this road has feedback. Check it out:

So, in UserProfile.js:

  processSubmission(formStatus: Object, fields: Object) {
    if (!formStatus.valid) {
      this.setState({
        dialogMessage:
          'Some fields in the form are invalid. Please correct them.',
      });
    } else {
      this.props.updateUserProfile({
        email: fields.email.value,
        firstName: fields.firstName.value,
        lastName: fields.lastName.value,
        password: fields.password.value,
      }, (error: Object) => {
        if (!error) console.log('done');
        else console.error(error);
      });
    }
  }

And in UserProfileContainer.js (wraps UserProfile):

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

import UserProfile from '../components/UserProfile';
import { updateUserProfile as _updateUserProfile } from '../methods';

export default createContainer(() => {
  const updateUserProfile = (args: Object, callback: Function) => {
    _updateUserProfile.call({ ...args, userId: Meteor.userId() }, callback);
  };

  return {
    updateUserProfile,
    user: Meteor.user(),
  }
}, UserProfile)

With this approach, UserProfile is kept pure, and I’m free to stub the updateUserProfile prop when doing testing or using React Storybook. UserProfileContainer handles anything Meteor-oriented, and injects the userId into the arguments to be passed to the method.

If anyone has any better/cleaner approaches, I definitely wanna hear about it!


#2

So would you like to revisit React Container + Method? :slight_smile:


#3

Haha… yes! But it’s a good sign that I now disagree with my old self. :wink: It means I’ve learned things since then.


#4

Haha yeah, that’s the life of any good, forward looking programmer, argue for something like your life depends on it and then a few weeks later, act like it has never happened :smiley:


#5

Another cool alternative:

export default createContainer(() => {
  const updateUserProfile = (args) => {
    return new Promise((resolve, reject) => {
      _updateUserProfile.call({ ...args, userId: Meteor.userId() }, (error, result) => {
        // Better error-handling than this, probably
        if (!error) resolve(result);
        else reject(error.reason);
      });
    });
  };

  return {
    updateUserProfile,
    user: Meteor.user(),
  }
}, UserProfile)

Then in the React component class:

async doSomething() {
  try {
    const result = await updateUserProfile({ name: 'Bob' });
    // Do stuff
  } catch (e) {
    console.log('Error!', e.reason);
  }
}