Using mixins vs components for code reuse in Facebook React

javascript, mixins, reactjs, refactoring

Solution

Update: this answer is outdated. Stay away from the mixins if you can. I warned you! Mixins Are Dead. Long Live Composition

At first, I tried to use subcomponents for this and extract `FormWidget` and `InputWidget`. However, I abandoned this approach halfway because I wanted a better control over generated `input`s and their state.

Two articles that helped me most:

- Thinking in React made me realize I don't actually want nested components for this;

- Reusable Components has a neat mixin example.

It turned out to that I only needed to write two (different) mixins: `ValidationMixin` and `FormMixin`. Here's how I separated them.

ValidationMixin

Validation mixin adds convenience methods to run your validator functions on some of your state's properties and store “error'd” properties in a `state.errors` array so you can highlight corresponding fields.

Source (gist)

define(function () {

  'use strict';

  var _ = require('underscore');

  var ValidationMixin = {
    getInitialState: function () {
      return {
        errors: []
      };
    },

    componentWillMount: function () {
      this.assertValidatorsDefined();
    },

    assertValidatorsDefined: function () {
      if (!this.validators) {
        throw new Error('ValidatorMixin requires this.validators to be defined on the component.');
      }

      _.each(_.keys(this.validators), function (key) {
        var validator = this.validators[key];

        if (!_.has(this.state, key)) {
          throw new Error('Key "' + key + '" is defined in this.validators but not present in initial state.');
        }

        if (!_.isFunction(validator)) {
          throw new Error('Validator for key "' + key + '" is not a function.');
        }
      }, this);
    },

    hasError: function (key) {
      return _.contains(this.state.errors, key);
    },

    resetError: function (key) {
      this.setState({
        'errors': _.without(this.state.errors, key)
      });
    },

    validate: function () {
      var errors = _.filter(_.keys(this.validators), function (key) {
        var validator = this.validators[key],
            value = this.state[key];

        return !validator(value);
      }, this);

      this.setState({
        'errors': errors
      });

      return _.isEmpty(errors);
    }
  };

  return ValidationMixin;

});

Usage

`ValidationMixin` has three methods: `validate`, `hasError` and `resetError`. It expects class to define `validators` object, similar to `propTypes`:

var JoinWidget = React.createClass({
  mixins: [React.addons.LinkedStateMixin, ValidationMixin, FormMixin],

  validators: {
    email: Misc.isValidEmail,
    name: function (name) {
      return name.length > 0;
    }
  },

  // ...

});

When user presses the submission button, I call `validate`. A call to `validate` will run each validator and populate `this.state.errors` with an array that contains keys of the properties that failed validation.

In my `render` method, I use `hasError` to generate correct CSS class for fields. When user puts focus inside the field, I call `resetError` to remove error highlight till next `validate` call.

renderInput: function (key, options) {
  var classSet = {
    'Form-control': true,
    'Form-control--error': this.hasError(key)
  };

  return (
    <input key={key}
           type={options.type}
           placeholder={options.placeholder}
           className={React.addons.classSet(classSet)}
           valueLink={this.linkState(key)}
           onFocus={_.partial(this.resetError, key)} />
  );
}

FormMixin

Form mixin handles form state (editable, submitting, submitted). You can use it to disable inputs and buttons while request is being sent, and to update your view correspondingly when it is sent.

Source (gist)

define(function () {

  'use strict';

  var _ = require('underscore');

  var EDITABLE_STATE = 'editable',
      SUBMITTING_STATE = 'submitting',
      SUBMITTED_STATE = 'submitted';

  var FormMixin = {
    getInitialState: function () {
      return {
        formState: EDITABLE_STATE
      };
    },

    componentDidMount: function () {
      if (!_.isFunction(this.sendRequest)) {
        throw new Error('To use FormMixin, you must implement sendRequest.');
      }
    },

    getFormState: function () {
      return this.state.formState;
    },

    setFormState: function (formState) {
      this.setState({
        formState: formState
      });
    },

    getFormError: function () {
      return this.state.formError;
    },

    setFormError: function (formError) {
      this.setState({
        formError: formError
      });
    },

    isFormEditable: function () {
      return this.getFormState() === EDITABLE_STATE;
    },

    isFormSubmitting: function () {
      return this.getFormState() === SUBMITTING_STATE;
    },

    isFormSubmitted: function () {
      return this.getFormState() === SUBMITTED_STATE;
    },

    submitForm: function () {
      if (!this.isFormEditable()) {
        throw new Error('Form can only be submitted when in editable state.');
      }

      this.setFormState(SUBMITTING_STATE);
      this.setFormError(undefined);

      this.sendRequest()
        .bind(this)
        .then(function () {
          this.setFormState(SUBMITTED_STATE);
        })
        .catch(function (err) {
          this.setFormState(EDITABLE_STATE);
          this.setFormError(err);
        })
        .done();
    }
  };

  return FormMixin;

});

Usage

It expects component to provide one method: `sendRequest`, which should return a Bluebird promise. (It's trivial to modify it to work with Q or other promise library.)

It provides convenience methods such as `isFormEditable`, `isFormSubmitting` and `isFormSubmitted`. It also provides a method to kick off the request: `submitForm`. You can call it from form buttons' `onClick` handler.

Problem

I'm beginning to use Facebook React in a Backbone project and so far it's going really well. However, I noticed some duplication creeping into my React code. For example, I have several form-like widgets with states like `INITIAL`, `SENDING` and `SENT`. When a button is pressed, the form needs to be validated, a request is made, and then state is updated. State is kept inside React `this.state` of course, along with field values. If these were Backbone views, I would have extracted a base class called `FormView` but my impression was that React neither endorses nor supports subclassing to share view logic (correct me if I'm wrong). I've seen two approaches to code reuse in React: - Mixins (like LinkedStateMixin that ships with React); - Container Components (such as react-infinite-scroll). Am I correct that mixins and containers are preferred to inheritance in React? Is this a deliberate design decision? Would it make more sense to use a mixin or a container component for my “form widget” example from second paragraph? Here's a gist with `FeedbackWidget` and `JoinWidget` in their current state. They have a similar structure, similar `beginSend` method and will both need to have some validation support (not there yet).

Original source