Skip to content

Commit 9793020

Browse files
logandavishswolff
authored andcommitted
fix: adds check for null model to componentWillReceiveProps (#21)
* fix: adds check for null model to componentWillReceiveProps There are some cases where a Backbone model passed through CBR might become undefined. (Consider the case where a view relies on a secondary model as a "decorator", and the use of that "decorator" model is toggled on or off.) Today, when a model becomes undefined, CBR throws a TypeError in ComponentWillReceiveProps: Uncaught TypeError: Cannot read property 'on' of undefined This commit adds a check for null or undefined models before trying to set event listeners in componentWillReceiveProps. * Removes listeners from models which become undefined * DRYs removeEventListener code * Reference model directly in componentWillReceiveProps * Checks this.context.models for models becoming undefined Previously, we only checked this.props.models for models becoming undefined in componentWillReceiveProps. This commit also checks this.context.models, but only if a matching model was not found in this.props.models -- because of the Object.assign in setModels, if there are two models with the same mapKey, we prefer the one in props. * Adds test for model becoming undefined in componentWillReceiveProps * Micro-optimize and clean componentWillReceiveProps * Refactor undefined model test per review * Adds testing for initial model listener bindings * Adds tests: createListener and removeListener are called for decorator model * Updates test titles and comments per review
1 parent 1c1c265 commit 9793020

File tree

2 files changed

+99
-7
lines changed

2 files changed

+99
-7
lines changed

lib/connect-backbone-to-react.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,12 @@ module.exports = function connectBackboneToReact(
107107
});
108108
}
109109

110+
removeEventListener(modelName, model) {
111+
getEventNames(modelName).forEach(name => {
112+
model.off(name, this.createNewProps, this);
113+
});
114+
}
115+
110116
createNewProps() {
111117
// Bail out if our component has been unmounted.
112118
// The only case where this flag is encountered is when this component
@@ -138,11 +144,28 @@ module.exports = function connectBackboneToReact(
138144
// Bind event listeners for each model that changed.
139145
Object.keys(this.models).forEach(mapKey => {
140146
const model = this.models[mapKey];
141-
if ((this.props.models && this.props.models[mapKey] === this.models[mapKey]) ||
142-
(this.context.models && this.context.models[mapKey] === this.models[mapKey])) {
143-
return; // Did not change.
147+
148+
// Retrieve old versions of the model from props and context for comparison.
149+
const propsModel = this.props.models ? this.props.models[mapKey] : undefined;
150+
const contextModel = this.context.models ? this.context.models[mapKey] : undefined;
151+
152+
// Do not attempt to create event listeners on an undefined model.
153+
if (!model) {
154+
// Instead, if it was previously defined, remove the old listeners.
155+
if (propsModel) {
156+
this.removeEventListener(mapKey, propsModel);
157+
// If a model with the matching mapKey exists in both props and context,
158+
// we only remove listeners from the one in props. We do this because
159+
// only the one in props is actually used in this.models, per the
160+
// Object.assign in setModel.
161+
} else if (contextModel) {
162+
this.removeEventListener(mapKey, contextModel);
163+
}
164+
return;
144165
}
145166

167+
if ((propsModel === model) || (contextModel === model)) return; // Did not change.
168+
146169
this.createEventListener(mapKey, model);
147170
});
148171
}
@@ -156,10 +179,7 @@ module.exports = function connectBackboneToReact(
156179
const model = this.models[mapKey];
157180
// Do not attempt to remove event listeners on an undefined model.
158181
if (!model) return;
159-
160-
getEventNames(mapKey).forEach(name => {
161-
model.off(name, this.createNewProps, this);
162-
});
182+
this.removeEventListener(mapKey, model);
163183
});
164184

165185
this.hasBeenUnmounted = true;

test/connect-backbone-to-react.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,78 @@ describe('connectBackboneToReact', function() {
594594
});
595595
});
596596

597+
describe('when passed props change to include', function() {
598+
let ConnectedTest;
599+
let setStateSpy;
600+
let createListenerSpy;
601+
let removeListenerSpy;
602+
603+
beforeEach(function() {
604+
ConnectedTest = connectBackboneToReact(mapModelsToProps)(TestComponent);
605+
wrapper = mount(<ConnectedTest models={modelsMap} />);
606+
607+
setStateSpy = sandbox.spy(ConnectedTest.prototype, 'setState');
608+
createListenerSpy = sandbox.spy(ConnectedTest.prototype, 'createEventListener');
609+
removeListenerSpy = sandbox.spy(ConnectedTest.prototype, 'removeEventListener');
610+
611+
const decoratorUserModel = new UserModel({
612+
name: 'Robert',
613+
age: '30',
614+
hungry: false,
615+
});
616+
617+
const initialModelsMap = {
618+
user: userModel,
619+
coll: userCollection,
620+
decorator: decoratorUserModel,
621+
};
622+
623+
wrapper.setProps({ models: initialModelsMap });
624+
});
625+
626+
afterEach(function() {
627+
wrapper.unmount();
628+
});
629+
630+
it('calls setState once', function() {
631+
assert.equal(setStateSpy.callCount, 1);
632+
});
633+
634+
it('calls createEventListener once due to decoratorUserModel being added as a model', function() {
635+
assert.equal(createListenerSpy.callCount, 1);
636+
assert.equal(createListenerSpy.firstCall.args[0], 'decorator');
637+
});
638+
639+
it('does not call removeEventListener', function() {
640+
assert.equal(removeListenerSpy.callCount, 0);
641+
});
642+
643+
describe('an undefined model', function() {
644+
beforeEach(function() {
645+
const newModelsMap = {
646+
user: userModel,
647+
coll: userCollection,
648+
decorator: undefined,
649+
};
650+
651+
wrapper.setProps({ models: newModelsMap });
652+
});
653+
654+
it('calls setState again', function() {
655+
assert.equal(setStateSpy.callCount, 2);
656+
});
657+
658+
it('does not call createEventListener again', function() {
659+
assert.equal(createListenerSpy.callCount, 1);
660+
});
661+
662+
it('calls removeEventListener once for decoratorUserModel', function() {
663+
assert.equal(removeListenerSpy.callCount, 1);
664+
assert.equal(removeListenerSpy.firstCall.args[0], 'decorator');
665+
});
666+
});
667+
});
668+
597669
describe('when unmounted in an event listener and subscribed to "all" event', function() {
598670
// To add more color, "all" event handlers are triggered after individual event handlers.
599671
// That is to say, if you trigger "foo" the sequence of event handlers called is:

0 commit comments

Comments
 (0)