Can I avoid forceUpdate() when using React with Backbone?
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.
Dan Abramov
Co-creator: Redux, Create React App. Github: https://github.com/gaearon Twitter: https://twitter.com/dan_abramov
Updated on June 08, 2022Comments
-
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 fromthis.props
andthis.state
inrender()
. 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 callforceUpdate
:- http://www.thomasboyt.com/2013/12/17/using-reactjs-as-a-backbone-view.html
- https://github.com/usepropeller/react.backbone/blob/master/react.backbone.js
- https://gist.github.com/ssorallen/7883081
- http://eldar.djafarov.com/2013/11/reactjs-mixing-with-backbone/
Even React's own example uses
forceUpdate
:Is there a better way, though, and what benefits would it give?
-
Dan Abramov over 10 yearsThanks a lot! I've found Backbone.React.Component but it uses
setProps
withtoJSON
. From my understanding, I can use their approach, but usesetState
instead. -
Sophie Alpert over 10 yearsThat'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 over 10 yearsIf 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 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'spreviousAttributes()
would be the wrong thing.) -
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 over 10 yearsWhat 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 downtoJSON
results asprops
. When backbone model changes, I callrenderComponent
again and recreate the whole component hierarchy (which isn't costly since it still updates DOM smartly). I still avoidsetProps
because I prefer to think of props as of set-once property. -
Admin about 10 yearsIt's more or less the approach I'm taking on the library. Though I use setProps on the top level component.
-
Nicholas Meyer about 10 yearsHow 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 about 10 yearsPerhaps 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 over 9 yearsI'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 over 9 yearsActually, if you bind to
forceUpdate()
onchange
(oradd remove reset
, for collections), and all your props are Backbone Models or Collections, can't you then usePureRenderMixin
? (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 checkshouldComponentUpdate
unless it's a child of a changed node? -
winduptoy about 9 yearsWhat if I have a component that's listening to multiple Backbone models? Inevitably they'll both fire
change
at the same moment and then callforceUpdate()
in the middle of a render, and I'll get an "Invariant Violation" error saying I can't callforceUpdate
within a render. Do I just need to listen to both of them and debounce theforceUpdate
? -
Alexander Mills about 8 yearsBackbone set/get is useful because you know when something has changed and therefore know whether you need to save or not
-
jyek over 7 yearsDoes anyone know how to implement this using a higher order component rather than a mixin?