Fix ReactTransitionGroup behavior when removing several children at once
If several children complete leaving before rendering TransitionGroup, only the last one was removed. This could easily happen if callback in componentWillLeave is called synchronously and several items are removed from array. The other case is when ReactCSSTransitionGroup has transitionLeave={false} and array is also cleaned up. The bug was happening because this.state.children was used as a base for children removal and it wasn't updated until the render, so only the last removal was actually happening. Fix involves keeping the updated children state between invocations of _handleDoneLeaving. After updating this.state and rendering, updatedState is cleaned up and ready for subsequent array modifications. Test case included.
Alexander Shtuchkin committed
Nov 17, 2014 at 14:50 UTC
c779ad4da3c2895cc9d9b65722eb5b3c5750e529
2 files changed
+70
-3
src/addons/transitions/ReactTransitionGroup.js
+9
-3
@@ -89,6 +89,8 @@ var ReactTransitionGroup = React.createClass({
89
},
90
91
componentDidUpdate: function() {
92
+ this.updatedChildren = null;
93
+
94
var keysToEnter = this.keysToEnter;
95
this.keysToEnter = [];
96
keysToEnter.forEach(this.performEnter);
@@ -193,9 +195,13 @@ var ReactTransitionGroup = React.createClass({
195
// This entered again before it fully left. Add it again.
196
this.performEnter(key);
197
} else {
196
- var newChildren = assign({}, this.state.children);
197
- delete newChildren[key];
198
- this.setState({children: newChildren});
198
+ // As this.state.children will not be updated until next render, we keep
199
+ // this.updatedChildren state to avoid losing all but the last removal.
200
+ // It's cleaned after this.state is updated, in componentDidUpdate.
201
+ if (!this.updatedChildren)
202
+ this.updatedChildren = assign({}, this.state.children);
203
+ delete this.updatedChildren[key];
204
+ this.setState({children: this.updatedChildren});
205
}
206
},
207
src/addons/transitions/__tests__/ReactTransitionGroup-test.js
+61
@@ -208,4 +208,65 @@ describe('ReactTransitionGroup', function() {
208
'didMount', 'didMount', 'willEnter', 'didEnter'
209
]);
210
});
211
+
212
+ it('should handle entering/leaving several elements at once', function() {
213
+ var log = [];
214
+ var cb;
215
+
216
+ var Child = React.createClass({
217
+ componentDidMount: function() {
218
+ log.push('didMount'+this.props.id);
219
+ },
220
+ componentWillEnter: function(cb) {
221
+ log.push('willEnter'+this.props.id);
222
+ cb();
223
+ },
224
+ componentDidEnter: function() {
225
+ log.push('didEnter'+this.props.id);
226
+ },
227
+ componentWillLeave: function(cb) {
228
+ log.push('willLeave'+this.props.id);
229
+ cb();
230
+ },
231
+ componentDidLeave: function() {
232
+ log.push('didLeave'+this.props.id);
233
+ },
234
+ componentWillUnmount: function() {
235
+ log.push('willUnmount'+this.props.id);
236
+ },
237
+ render: function() {
238
+ return <span />;
239
+ }
240
+ });
241
+
242
+ var Component = React.createClass({
243
+ getInitialState: function() {
244
+ return {count: 1};
245
+ },
246
+ render: function() {
247
+ var children = [];
248
+ for (var i = 0; i < this.state.count; i++) {
249
+ children.push(<Child key={i} id={i} />);
250
+ }
251
+ return <ReactTransitionGroup>{children}</ReactTransitionGroup>;
252
+ }
253
+ });
254
+
255
+ var instance = React.render(<Component />, container);
256
+ expect(log).toEqual(['didMount0']);
257
+ log = [];
258
+
259
+ instance.setState({count: 3});
260
+ expect(log).toEqual([
261
+ 'didMount1', 'didMount2', 'willEnter1', 'didEnter1',
262
+ 'willEnter2', 'didEnter2'
263
+ ]);
264
+ log = [];
265
+
266
+ instance.setState({count: 0});
267
+ expect(log).toEqual([
268
+ 'willLeave0', 'didLeave0', 'willLeave1', 'didLeave1',
269
+ 'willLeave2', 'didLeave2', 'willUnmount0', 'willUnmount1', 'willUnmount2'
270
+ ]);
271
+ });
272
});