Jof Arnold and Benjie Gillam
Timecounts.org
Storing state in DOM
Layout changes breaking jQuery selectors
class='_element_to_reference'
and other hacks
Re-rendering entire views
... rebuilding absolutely not an option
// ----- Backbone Router -----var AppRouter = Backbone.Router.extend({ initialize: function() { this.$rootEl = $("#content"); this.rootEl = this.$rootEl[0]; }, setView: function(view) { if (this.view) { this.view.remove(); } this.view = view; this.view.render(); this.$rootEl.append(this.view.el); }, // ---------- routes: { '': 'homeRoute', 'about': 'aboutRoute' }, homeRoute: function() { this.setView(new HomeView()) }, aboutRoute: function() { this.setView(new AboutView()) }});
// ----- About View -----var AboutView = Backbone.View.extend({ template: _.template('<h1>About</h1><p>' + '<img src="http://www.reactiongifs.com/r/1gjdAX7.gif"></p>'), render: function() { this.$el.html(this.template()); }});
// ----- Home View -----var HomeView = Backbone.View.extend({ template: _.template('<h1>Hello World!</h1>' + '<p>Random number: <span><%- number %></span> (click for another)</p>' + '<textarea>Notes...</textarea>'), events: { 'click p': 'render' }, randomNumber: function() { return Math.floor(Math.random() * 100) + 1; }, render: function() { this.$el.html(this.template({ number: this.randomNumber() })); }});
Calling render()
directly will overwrite everything, including our
<textarea>
, resetting its content
Instead we need to update just the <span>
containing the random number:
// ----- Home View -----var HomeView = Backbone.View.extend({ template: _.template('<h1>Hello World!</h1>' + '<p>Random number: <span><%- number %></span> (click for another)</p>' + '<textarea>Notes...</textarea>'), events: { 'click p': 'newNumber' }, randomNumber: function() { return Math.floor(Math.random() * 100) + 1; }, newNumber: function(e) { this.$("span").html(this.randomNumber()); }, render: function() { this.$el.html(this.template({ number: this.randomNumber() })); }});
AppRouter = Backbone.Router.extend({ //... setView: function(view) { if (this.view) { if (this.view instanceof Backbone.View) { this.view.remove(); } else { React.unmountComponentAtNode(this.rootEl); } } this.view = view; if (this.view instanceof Backbone.View) { this.view.render(); this.$rootEl.append(this.view.el); } else { React.render(this.view, this.rootEl); } }, //...});
var DOM = React.DOM;var HomeView = React.createClass({ displayName: "HomeView", getInitialState: function() { return { randomNumber: this.randomNumber() }; }, randomNumber: function() { return Math.floor(Math.random() * 100) + 1; }, newNumber: function(e) { this.setState({ randomNumber: this.randomNumber() }); }, render: function() { return DOM.div(null, DOM.h1(null, "Hello World!"), DOM.p({ onClick: this.newNumber }, "Random number: " + this.state.randomNumber + " (click for another)"), DOM.textarea(null, "Notes...") ) }});
randomNumber
now stored in this.state
: value accessible without reading the DOM
No fragile jQuery find-tag-then-replace-html nonsense
Using a small mixin we can have React automatically re-render when any model/collection changes...
... so long as we store it on this.state
We track event listeners via a Backbone.Events
object.
function BackboneListener(){}_.extend(BackboneListener.prototype, Backbone.Events);var BackboneMixin = { getInitialState: function() { return { _backboneListener: new BackboneListener() }; },
Hook the relevant lifecycle methods to keep subscriptions current and avoid leaks
componentDidMount: function() { this._backboneSubscribeObjects(this.state); }, componentWillUpdate: function(nextProps, nextState) { this._backboneUnsubscribeObjects(this.state); this._backboneSubscribeObjects(nextState); }, componentWillUnmount: function() { this.state._backboneListener.stopListening(); },
(Un)subscribing with _backboneListener
_backboneSubscribeObjects: function(hash, unsubscribe) { var method = (unsubscribe === true ? 'stopListening' : 'listenTo'); var listener = this.state._backboneListener; for (var key in hash) { var obj = hash[key]; if (obj instanceof Backbone.Model) { listener[method](obj, "change", this._forceUpdateOnce); } else if (obj instanceof Backbone.Collection) { listener[method](obj, "add remove reset sort change destroy sync", this._forceUpdateOnce); } } }, _backboneUnsubscribeObjects: function(hash) { this._backboneSubscribeObjects(hash, true); },
For efficiency, we don't call this.forceUpdate()
for every single change - instead at most once per runloop
_forceUpdateOnce: function() { if (this._forceUpdateOnceTimer) return; var _this = this; this._forceUpdateOnceTimer = setTimeout(function() { delete _this._forceUpdateOnceTimer; if (_this.isMounted()) { return _this.forceUpdate(); } }, 0); },
Caveat: this makes cascading model/collection changes more efficient; however the update is not real time...
So if you have a managed input element with value: backboneModel.get('property')
you must call this.forceUpdate()
in your onChange
handler after updating the model - e.g.
changedText: function(e) { model.set('text', this.refs.text.getDOMNode().value); this.forceUpdate(); //<-- VITAL},render: function() { return DOM.input({ref: 'text', value: this.state.model.get('text'), onChange: this.changeText });}
Bonus helper methods, in case you want to hook the 'sync'
event on a collection to refetch a dependent collection or similar
listenTo: function() { var listener = this.state._backboneListener; return listener.listenTo.apply(listener, arguments); }, stopListening: function() { var listener = this.state._backboneListener; return listener.stopListening.apply(listener, arguments); }};
this.listenTo()
will be automatically unsubscribed when your component is unmounted - often this means there's no need to manually call this.stopListening()
)mixins
arrayvar HomeView = React.createClass({ displayName: "HomeView", mixins: [BackboneMixin], getInitialState: function() { return { model: new MyBackboneModel(), randomNumber: this.randomNumber() }; }, //...});
props
to state
If your framework passes data to its root view via this.props
then we need to ensure that models and collections are copied over to this.state
BackboneMixin
monitors for changes on this.state
onlyvar BackbonePropsMixin = { getInitialState: function() { return _.pick(this.props, this._subscribableObject); }, componentWillReceiveProps: function(nextProps) { var changes = {}, k = null, v = null; for (k in nextProps) { v = nextProps[k]; if (this.state[k] !== v && this.props._subscribableObject(v)) { changes[k] = v; } } for (k in this.props) { v = this.props[k]; if (this.state[k] === v && !nextProps[k] && this.props._subscribableObject(v)) { changes[k] = null; } } return this.setState(changes); } _subscribableObject: (o) { return (o instanceof Backbone.Model) || (o instanceof Backbone.Collection); }};
Only use BackboneMixin
where you have models on your this.state
that you'd like to listen to changes to...
... and only use BackbonePropsMixin
on top level components
If you subscribe to the same model in multiple places then React will be forced to redundantly render branches of the view tree multiple times due to the use of this.forceRender()
Don't need to tear everything up and start again
Policy of all new views in React
BackboneMixin
auto-subscribes to events
Use BackbonePropsMixin
only if necessary
Easier to maintain code - and less of it!
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
w | Pause/Resume the presentation |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |