Skip to content

Commit 213e243

Browse files
joepvldanez
authored andcommitted
Allow setState in onSelect (fixes #51) (#110)
* Add a failing test for #51. This shows that currently calling `setState` in `onChange` prevents changing tabs. * Fix #51. # What? This changes the `copyPropsToState` method to take a second argument (`state`), enabling using it when feeding `setState` a callback so we can do a transactional update in `componentWillReceiveProps`. # Why? This is needed because previously we were hitting a race condition whenever the parent component was triggering a rerender when using `setState` in its `onSelect` handler: `componentWillReceiveProps` would be triggered, calling `copyPropsToState`, which in turn read from `this.state` synchronously before the state changes triggered in `Tabs`'s own `setSelected` method had been committed to the state. * Add a `setState` call in the focus example.
1 parent ebc97eb commit 213e243

File tree

3 files changed

+45
-6
lines changed

3 files changed

+45
-6
lines changed

examples/focus/app.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@ import ReactDOM from 'react-dom';
33
import { Tab, Tabs, TabList, TabPanel } from '../../src/main';
44

55
const App = React.createClass({
6-
handleInputChange() {
6+
getInitialState() {
7+
return { inputValue: '' };
8+
},
9+
10+
handleInputChange(e) {
711
this.forceUpdate();
12+
this.setState({ inputValue: e.target.value });
813
},
914

1015
render() {
@@ -23,6 +28,7 @@ const App = React.createClass({
2328
<input
2429
type="text"
2530
onChange={this.handleInputChange}
31+
value={this.state.inputValue}
2632
/>
2733
</TabPanel>
2834
</Tabs>

src/components/Tabs.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ module.exports = React.createClass({
4848
},
4949

5050
getInitialState() {
51-
return this.copyPropsToState(this.props);
51+
return this.copyPropsToState(this.props, this.state);
5252
},
5353

5454
getChildContext() {
@@ -64,7 +64,10 @@ module.exports = React.createClass({
6464
},
6565

6666
componentWillReceiveProps(newProps) {
67-
this.setState(this.copyPropsToState(newProps));
67+
// Use a transactional update to prevent race conditions
68+
// when reading the state in copyPropsToState
69+
// See https://github.com/reactjs/react-tabs/issues/51
70+
this.setState(state => this.copyPropsToState(newProps, state));
6871
},
6972

7073
setSelected(index, focus) {
@@ -282,7 +285,7 @@ module.exports = React.createClass({
282285
},
283286

284287
// This is an anti-pattern, so sue me
285-
copyPropsToState(props) {
288+
copyPropsToState(props, state) {
286289
let selectedIndex = props.selectedIndex;
287290

288291
// If no selectedIndex prop was supplied, then try
@@ -294,8 +297,8 @@ module.exports = React.createClass({
294297
// Manual testing can be done using examples/focus
295298
// See 'should preserve selectedIndex when typing' in specs/Tabs.spec.js
296299
if (selectedIndex === -1) {
297-
if (this.state && this.state.selectedIndex) {
298-
selectedIndex = this.state.selectedIndex;
300+
if (state && state.selectedIndex) {
301+
selectedIndex = state.selectedIndex;
299302
} else {
300303
selectedIndex = 0;
301304
}

src/components/__tests__/Tabs-test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,34 @@ describe('react-tabs', () => {
294294
wrapper.childAt(0).childAt(2).simulate('click');
295295
assertTabSelected(wrapper, 0);
296296
});
297+
298+
it('should switch tabs if setState is called within onSelect', () => {
299+
class Wrap extends React.Component {
300+
constructor(props, state) {
301+
super(props, state);
302+
303+
this.state = {
304+
foo: 'foo',
305+
};
306+
307+
this.handleSelect = this.handleSelect.bind(this);
308+
}
309+
310+
handleSelect() {
311+
this.setState({ foo: 'bar' });
312+
}
313+
314+
render() {
315+
return createTabs({ onSelect: this.handleSelect });
316+
}
317+
}
318+
319+
const wrapper = mount(<Wrap />);
320+
321+
wrapper.childAt(0).childAt(1).simulate('click');
322+
assertTabSelected(wrapper, 1);
323+
324+
wrapper.childAt(0).childAt(2).simulate('click');
325+
assertTabSelected(wrapper, 2);
326+
});
297327
});

0 commit comments

Comments
 (0)