Can I avoid forceUpdate() when using React with Backbone?

17,385

Solution 1

Pete's answer is great.

Backbone models are inherently mutative, which (while not a problem in itself) means that when rerendering, you won't have the old version of the model to compare to. This makes it harder to do intelligent optimizations by defining shouldComponentUpdate methods in key places on your components. (You also lose out on the ability to easily store old versions of your model for other reasons, like implementing undo.)

Calling forceUpdate merely skips shouldComponentUpdate and forces the component to rerender. Note that calling render is usually cheap, and React will still only touch the DOM if the output of render has changed, so performance problems here aren't common. However, if you have the choice to use immutable data (including passing around raw model property objects from toJSON() as Pete suggests), I'd highly recommend it.

Solution 2

Until there is a better answer, let me quote Pete Hunt, a core React developer:

The big win with Backbone models was it managed your data flow for you. When you called set() it would inform your app that data changed. With React you'll find this to be less necessary because all you need to do is inform the component that owns the state via a callback and React ensures that all children are up-to-date. So this part of backbone is less useful IMO (and people tend to use backbone in this way with React anyway).

You don't have to pass pure JSON (though that's what I tend to do and it works well for simple data models), but you will see lots of advantages if you keep your objects immutable.

You can try this out by just calling toJSON() on your backbone models and seeing how you like it vs passing the models around.

(emphasis mine)

Interestingly, Backbone.React.Component is the only example I found that uses toJSON, but for some reason also uses setProps instead of setState (which is discouraged too).

Update

I made a simple mixin based on Pete Hunt's approach (no setProps, no forceUpdate):

define(function () {

  'use strict';

  var Backbone = require('backbone'),
      _ = require('underscore');

  var BackboneStateMixin = {
    getInitialState: function () {
      return this.getBackboneState(this.props);
    },

    componentDidMount: function () {
      if (!_.isFunction(this.getBackboneState)) {
        throw new Error('You must provide getBackboneState(props).');
      }

      this._bindBackboneEvents(this.props);
    },

    componentWillReceiveProps: function (newProps) {
      this._unbindBackboneEvents();
      this._bindBackboneEvents(newProps);
    },

    componentWillUnmount: function () {
      this._unbindBackboneEvents();
    },

    _updateBackboneState: function () {
      var state = this.getBackboneState(this.props);
      this.setState(state);
    },

    _bindBackboneEvents: function (props) {
      if (!_.isFunction(this.watchBackboneProps)) {
        return;
      }

      if (this._backboneListener) {
        throw new Error('Listener already exists.');
      }

      if (!props) {
        throw new Error('Passed props are empty');
      }

      var listener = _.extend({}, Backbone.Events),
          listenTo = _.partial(listener.listenTo.bind(listener), _, _, this._updateBackboneState);

      this.watchBackboneProps(props, listenTo);
      this._backboneListener = listener;
    },

    _unbindBackboneEvents: function () {
      if (!_.isFunction(this.watchBackboneProps)) {
        return;
      }

      if (!this._backboneListener) {
        throw new Error('Listener does not exist.');
      }

      this._backboneListener.stopListening();
      delete this._backboneListener;
    }
  };

  return BackboneStateMixin;

});

It doesn't care about what kind of models or collections you have.

The convention is that Backbone models go in props and their JSON is automatically put by mixin into state. You need to override getBackboneState(props) for this to work, and optionally watchBackboneProps to tell the mixin when to call setState with fresh values.

Usage example:

var InfoWidget = React.createClass({
  mixins: [BackboneStateMixin, PopoverMixin],

  propTypes: {
    stampModel: React.PropTypes.instanceOf(Stamp).isRequired
  },

  // Override getBackboneState to tell the mixin
  // HOW to transform Backbone props into JSON state

  getBackboneState: function (props) {
    var stampModel = props.stampModel,
        primaryZineModel = stampModel.getPrimaryZine();

    return {
      stamp: stampModel.toJSON(),
      toggleIsLiked: stampModel.toggleIsLiked.bind(stampModel),
      primaryZine: primaryZineModel && primaryZineModel.toJSON()
    };
  },

  // Optionally override watchBackboneProps to tell the mixin
  // WHEN to transform Backbone props into JSON state

  watchBackboneProps: function (props, listenTo) {
    listenTo(props.stampModel, 'change:unauth_like_count change:is_liked');
    listenTo(props.stampModel.get('zines'), 'all');
  },

  render: function () {
    // You can use vanilla JSON values of this.state.stamp,
    // this.state.toggleIsLiked and this.state.primaryZine
    // or whatever you return from getBackboneState
    // without worrying they may point to old values
  }
}

Note: mixin requires Underscore 1.6.0+.

Solution 3

I'm the developer behind Backbone.React.Component. The reason why we're using setProps is because this is only intended to be called by the component owner (greatest parent). The way I see it, props is better to use for reactive updates (and to pass to child components) than state, but if you can point me some reasons why state is better I'll be happy to start developing towards that change.

For instance sometimes I've got components that delegate to others, where transferPropsTo is pretty handy. Using state kind of makes it harder to achieve that.

Share:
17,385
Dan Abramov
Author by

Dan Abramov

Co-creator: Redux, Create React App. Github: https://github.com/gaearon Twitter: https://twitter.com/dan_abramov

Updated on June 08, 2022

Comments

  • Dan Abramov
    Dan Abramov almost 2 years

    Facebook React encourages you to separate mutable (state) and immutable (props) state:

    Try to keep as many of your components as possible stateless. By doing this you'll isolate the state to its most logical place and minimize redundancy, making it easier to reason about your application.

    When the state changes, you are supposed to call setState to trigger virtual DOM diff, which will cause a real DOM update only when this is needed.

    There is a way to trigger DOM update manually by calling forceUpdate but it is discouraged:

    Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render(). This makes your application much simpler and more efficient.

    However, all React+Backbone examples I have seen ignore this advice and store models and collections in props and call forceUpdate:

    Even React's own example uses forceUpdate:

    Is there a better way, though, and what benefits would it give?

  • Dan Abramov
    Dan Abramov over 10 years
    Thanks a lot! I've found Backbone.React.Component but it uses setProps with toJSON. From my understanding, I can use their approach, but use setState instead.
  • Sophie Alpert
    Sophie Alpert over 10 years
    That's right, or instead of passing <Component model={backboneModel} /> you can do <Component info={backboneModel.toJSON()} /> or similar. (As far as I can tell, Backbone.React.Component loses the easy composability of components, which is one of React's main features. Using the model props for a component is interesting though.)
  • Edward M Smith
    Edward M Smith over 10 years
    If you're binding to the change event on the Backbone Model, you should have access to the previous state of the model and the list of properties changing. So, you should be able to implement an "intelligent optimization".
  • Sophie Alpert
    Sophie Alpert over 10 years
    @EdwardMSmith That's true, but if you store the model on props then I don't believe that actually helps you because you can't easily access the old and new attributes in shouldComponentUpdate. (In some cases, React will batch together multiple renders into one so Backbone's previousAttributes() would be the wrong thing.)
  • Edward M Smith
    Edward M Smith over 10 years
    @BenAlpert - True, you couldn't use shouldComponentUpdate, but you could do essentially the same thing in the Backbone.Model change handler - decide if you need to update and then call forceUpdate if you do. Not the best smell, tho.
  • Dan Abramov
    Dan Abramov over 10 years
    What I've come to myself is using state for internal component state that it doesn't share with anybody, but not for Backbone models. For Backbone models and collections, I pass down toJSON results as props. When backbone model changes, I call renderComponent again and recreate the whole component hierarchy (which isn't costly since it still updates DOM smartly). I still avoid setProps because I prefer to think of props as of set-once property.
  • Admin
    Admin about 10 years
    It's more or less the approach I'm taking on the library. Though I use setProps on the top level component.
  • Nicholas Meyer
    Nicholas Meyer about 10 years
    How does this work when you have nested models? If you pass a parent model to a view as JSON, then when it goes to pass a child model to a child view, it will be the full backbone model and not JSON, and hence mutable... (unless I'm misunderstanding how this is working)
  • Nicholas Meyer
    Nicholas Meyer about 10 years
    Perhaps the correct answer is I should be using model stores and for example on a one-to-many have a list of child ids on the parent model. Then subviews could do a lookup on those ids. Not sure if this is the way to fix the problem. Another question is whether you need a call to updateBackboneState in componentWillReceiveProps using the new props - it seems like that's necessary in many cases.
  • Blaine Hatab
    Blaine Hatab over 9 years
    I've been using your mixin and like it so far. I don't think it matters too much either way when you use backbone and react as you have. I did have a random question though, is it possible to cache the entire page if you are using backbone+react with your mixin on a static site generator. Been playing with this a lot lately and would love your feedback.
  • rattray
    rattray over 9 years
    Actually, if you bind to forceUpdate() on change (or add remove reset, for collections), and all your props are Backbone Models or Collections, can't you then use PureRenderMixin? (It seems to be working like a dream for me, anyway). Isn't this actually better than an immutable that triggers change events at the root (eg; Cortex) because you never even need to check shouldComponentUpdate unless it's a child of a changed node?
  • winduptoy
    winduptoy about 9 years
    What if I have a component that's listening to multiple Backbone models? Inevitably they'll both fire change at the same moment and then call forceUpdate() in the middle of a render, and I'll get an "Invariant Violation" error saying I can't call forceUpdate within a render. Do I just need to listen to both of them and debounce the forceUpdate?
  • Alexander Mills
    Alexander Mills about 8 years
    Backbone set/get is useful because you know when something has changed and therefore know whether you need to save or not
  • jyek
    jyek over 7 years
    Does anyone know how to implement this using a higher order component rather than a mixin?