Skip to content

Commit 3c9d61f

Browse files
authored
Merge pull request #157 from orgsync/ping-pong
Close #156 Add state stability check
2 parents 0b1542b + a785a71 commit 3c9d61f

3 files changed

Lines changed: 83 additions & 13 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ viewport.
171171

172172
## FAQ
173173

174-
##### Why is the list freezing/overflowing the stack?
174+
##### What is "ReactList failed to reach a stable state."?
175175

176176
This happens when specifying the `uniform` type without actually providing
177177
uniform size elements. The component attempts to draw only the minimum necessary
@@ -189,7 +189,7 @@ spacing.
189189

190190
##### Why is there no onScroll event handler?
191191

192-
If you need an onScroll handler, just add the handler to the div wrapping your ReactList component:
192+
If you need an onScroll handler, just add the handler to the div wrapping your ReactList component:
193193

194194
```
195195
<div style={{height: 300, overflow: 'auto'}} onScroll={this.handleScroll}>

react-list.es6

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ const PASSIVE = (() => {
3333
return hasSupport;
3434
})() ? {passive: true} : false;
3535

36+
const UNSTABLE_MESSAGE = 'ReactList failed to reach a stable state.';
37+
38+
const isEqualSubset = (a, b) => {
39+
for (let key in b) if (a[key] !== b[key]) return false;
40+
41+
return true;
42+
};
43+
44+
const isEqual = (a, b) => isEqualSubset(a, b) && isEqualSubset(b, a);
45+
3646
module.exports = class ReactList extends Component {
3747
static displayName = 'ReactList';
3848

@@ -72,6 +82,8 @@ module.exports = class ReactList extends Component {
7282
this.constrain(initialIndex, pageSize, itemsPerRow, this.props);
7383
this.state = {from, size, itemsPerRow};
7484
this.cache = {};
85+
this.prevPrevState = {};
86+
this.unstable = false;
7587
}
7688

7789
componentWillReceiveProps(next) {
@@ -85,15 +97,37 @@ module.exports = class ReactList extends Component {
8597
this.updateFrame(this.scrollTo.bind(this, this.props.initialIndex));
8698
}
8799

88-
componentDidUpdate() {
89-
this.updateFrame();
100+
componentDidUpdate(prevProps, prevState) {
101+
102+
// If the list has reached an unstable state, prevent an infinite loop.
103+
if (this.unstable) return;
104+
105+
// Update calculations if props have changed between renders.
106+
const propsEqual = isEqual(this.props, prevProps);
107+
if (!propsEqual) {
108+
this.prevPrevState = {};
109+
return this.updateFrame();
110+
}
111+
112+
// Check for ping-ponging between the same two states.
113+
const stateEqual = isEqual(this.state, prevState);
114+
const pingPong = !stateEqual && isEqual(this.state, this.prevPrevState);
115+
116+
// Ping-ponging between states means this list is unstable, log an error.
117+
if (pingPong) {
118+
this.unstable = true;
119+
return console.error(UNSTABLE_MESSAGE);
120+
}
121+
122+
// Update calculations if state has changed between renders.
123+
this.prevPrevState = prevState;
124+
if (!stateEqual) this.updateFrame();
90125
}
91126

92127
maybeSetState(b, cb) {
93-
const a = this.state;
94-
for (let key in b) if (a[key] !== b[key]) return this.setState(b, cb);
128+
if (isEqualSubset(this.state, b)) return cb();
95129

96-
cb();
130+
this.setState(b, cb);
97131
}
98132

99133
componentWillUnmount() {

react-list.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@
107107
return hasSupport;
108108
}() ? { passive: true } : false;
109109

110+
var UNSTABLE_MESSAGE = 'ReactList failed to reach a stable state.';
111+
112+
var isEqualSubset = function isEqualSubset(a, b) {
113+
for (var key in b) {
114+
if (a[key] !== b[key]) return false;
115+
}return true;
116+
};
117+
118+
var isEqual = function isEqual(a, b) {
119+
return isEqualSubset(a, b) && isEqualSubset(b, a);
120+
};
121+
110122
_module3.default.exports = (_temp = _class = function (_Component) {
111123
_inherits(ReactList, _Component);
112124

@@ -127,6 +139,8 @@
127139

128140
_this.state = { from: from, size: size, itemsPerRow: itemsPerRow };
129141
_this.cache = {};
142+
_this.prevPrevState = {};
143+
_this.unstable = false;
130144
return _this;
131145
}
132146

@@ -149,16 +163,38 @@
149163
}
150164
}, {
151165
key: 'componentDidUpdate',
152-
value: function componentDidUpdate() {
153-
this.updateFrame();
166+
value: function componentDidUpdate(prevProps, prevState) {
167+
168+
// If the list has reached an unstable state, prevent an infinite loop.
169+
if (this.unstable) return;
170+
171+
// Update calculations if props have changed between renders.
172+
var propsEqual = isEqual(this.props, prevProps);
173+
if (!propsEqual) {
174+
this.prevPrevState = {};
175+
return this.updateFrame();
176+
}
177+
178+
// Check for ping-ponging between the same two states.
179+
var stateEqual = isEqual(this.state, prevState);
180+
var pingPong = !stateEqual && isEqual(this.state, this.prevPrevState);
181+
182+
// Ping-ponging between states means this list is unstable, log an error.
183+
if (pingPong) {
184+
this.unstable = true;
185+
return console.error(UNSTABLE_MESSAGE);
186+
}
187+
188+
// Update calculations if state has changed between renders.
189+
this.prevPrevState = prevState;
190+
if (!stateEqual) this.updateFrame();
154191
}
155192
}, {
156193
key: 'maybeSetState',
157194
value: function maybeSetState(b, cb) {
158-
var a = this.state;
159-
for (var key in b) {
160-
if (a[key] !== b[key]) return this.setState(b, cb);
161-
}cb();
195+
if (isEqualSubset(this.state, b)) return cb();
196+
197+
this.setState(b, cb);
162198
}
163199
}, {
164200
key: 'componentWillUnmount',

0 commit comments

Comments
 (0)