Skip to content

Commit 5bfdead

Browse files
sspanakbkrem
authored andcommitted
react-transition-group is no longer used when transitions are disabled (#92)
* react-transition-group is no longer used when transitions are disabled, resulting in perf boost (#91) * eliminated unnecessary state changes in NodeWrapper * Ensures zoom listeners are rebound if NodeWrapper switched DOM nodes
1 parent f6edc78 commit 5bfdead

File tree

4 files changed

+131
-11
lines changed

4 files changed

+131
-11
lines changed

src/Tree/NodeWrapper.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { TransitionGroup } from 'react-transition-group';
4+
5+
export default class NodeWrapper extends React.Component {
6+
constructor(props) {
7+
super(props);
8+
this.state = {
9+
enableTransitions: props.transitionDuration > 0,
10+
};
11+
}
12+
13+
componentWillReceiveProps(nextProps) {
14+
if (nextProps.transitionDuration !== this.props.transitionDuration) {
15+
this.setState({
16+
enableTransitions: nextProps.transitionDuration > 0,
17+
});
18+
}
19+
}
20+
21+
render() {
22+
if (this.state.enableTransitions) {
23+
return (
24+
<TransitionGroup
25+
component={this.props.component}
26+
className={this.props.className}
27+
transform={this.props.transform}
28+
>
29+
{this.props.children}
30+
</TransitionGroup>
31+
);
32+
}
33+
34+
return (
35+
<g className={this.props.className} transform={this.props.transform}>
36+
{this.props.children}
37+
</g>
38+
);
39+
}
40+
}
41+
42+
NodeWrapper.defaultProps = {
43+
component: 'g',
44+
};
45+
46+
NodeWrapper.propTypes = {
47+
transitionDuration: PropTypes.number.isRequired,
48+
component: PropTypes.string,
49+
className: PropTypes.string.isRequired,
50+
transform: PropTypes.string.isRequired,
51+
children: PropTypes.array.isRequired,
52+
};

src/Tree/index.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
22
import PropTypes from 'prop-types';
3-
import { TransitionGroup } from 'react-transition-group';
43
import { layout, select, behavior, event } from 'd3';
54
import clone from 'clone';
65
import deepEqual from 'deep-equal';
76
import uuid from 'uuid';
87

8+
import NodeWrapper from './NodeWrapper';
99
import Node from '../Node';
1010
import Link from '../Link';
1111
import './style.css';
@@ -41,7 +41,12 @@ export default class Tree extends React.Component {
4141
this.internalState.initialRender = false;
4242
}
4343

44-
componentDidUpdate() {
44+
componentDidUpdate(prevProps) {
45+
// Rebind zoom listeners to new DOM nodes in case NodeWrapper switched <TransitionGroup> <-> <g>
46+
if (prevProps.transitionDuration !== this.props.transitionDuration) {
47+
this.bindZoomListener(this.props);
48+
}
49+
4550
if (typeof this.props.onUpdate === 'function') {
4651
this.props.onUpdate({
4752
node: this.internalState.targetNode ? clone(this.internalState.targetNode) : null,
@@ -366,7 +371,8 @@ export default class Tree extends React.Component {
366371
return (
367372
<div className={`rd3t-tree-container ${zoomable ? 'rd3t-grabbable' : undefined}`}>
368373
<svg className="rd3t-svg" width="100%" height="100%">
369-
<TransitionGroup
374+
<NodeWrapper
375+
transitionDuration={transitionDuration}
370376
component="g"
371377
className="rd3t-g"
372378
transform={`translate(${translate.x},${translate.y}) scale(${scale})`}
@@ -403,7 +409,7 @@ export default class Tree extends React.Component {
403409
styles={styles.nodes}
404410
/>
405411
))}
406-
</TransitionGroup>
412+
</NodeWrapper>
407413
</svg>
408414
</div>
409415
);

src/Tree/tests/NodeWrapper.test.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import React from 'react';
2+
import { shallow, mount } from 'enzyme';
3+
import { TransitionGroup } from 'react-transition-group';
4+
5+
import NodeWrapper from '../NodeWrapper';
6+
7+
describe('<NodeWrapper />', () => {
8+
jest.spyOn(NodeWrapper.prototype, 'setState');
9+
10+
// Clear method spies on prototype after each test
11+
afterEach(() => jest.clearAllMocks());
12+
13+
it('Renders a <g> when transitions are disabled', () => {
14+
const fixture = {
15+
transform: 't',
16+
className: 'cls',
17+
transitionDuration: 0,
18+
};
19+
20+
const renderedComponent = shallow(<NodeWrapper {...fixture}>{[]}</NodeWrapper>);
21+
expect(renderedComponent.find('g').prop('transform')).toContain(fixture.transform);
22+
expect(renderedComponent.find('g').prop('className')).toContain(fixture.className);
23+
});
24+
25+
it('Renders a <TransitionGroup> when transitions are enabled', () => {
26+
const fixture = {
27+
component: 'g',
28+
transform: 't',
29+
className: 'cls',
30+
transitionDuration: 10,
31+
};
32+
33+
const renderedComponent = shallow(<NodeWrapper {...fixture}>{[]}</NodeWrapper>);
34+
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(fixture.transform);
35+
expect(renderedComponent.find(TransitionGroup).prop('className')).toContain(fixture.className);
36+
expect(renderedComponent.find(TransitionGroup).prop('component')).toContain(fixture.component);
37+
});
38+
39+
it('does not do useless state updates unless transitionDuration has changed', () => {
40+
const fixture = {
41+
transform: 't',
42+
className: 'cls',
43+
transitionDuration: 1,
44+
};
45+
46+
const renderedComponent = mount(<NodeWrapper {...fixture}>{[]}</NodeWrapper>);
47+
renderedComponent.setProps(fixture);
48+
expect(renderedComponent.instance().setState).toHaveBeenCalledTimes(0);
49+
renderedComponent.setProps({
50+
...fixture,
51+
transitionDuration: 2,
52+
});
53+
expect(renderedComponent.instance().setState).toHaveBeenCalledTimes(1);
54+
});
55+
});

src/Tree/tests/index.test.js

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import React from 'react';
2-
import { TransitionGroup } from 'react-transition-group';
32
import { shallow, mount } from 'enzyme';
43
import { render } from 'react-dom';
54

5+
import NodeWrapper from '../NodeWrapper';
66
import Node from '../../Node';
77
import Link from '../../Link';
88
import Tree from '../index';
@@ -109,7 +109,7 @@ describe('<Tree />', () => {
109109
const fixture = { x: 123, y: 321 };
110110
const expected = `translate(${fixture.x},${fixture.y})`;
111111
const renderedComponent = shallow(<Tree data={mockData} translate={fixture} />);
112-
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(expected);
112+
expect(renderedComponent.find(NodeWrapper).prop('transform')).toContain(expected);
113113
});
114114
});
115115

@@ -216,26 +216,26 @@ describe('<Tree />', () => {
216216
const zoomLevel = 0.3;
217217
const expected = `scale(${zoomLevel})`;
218218
const renderedComponent = shallow(<Tree data={mockData} zoom={zoomLevel} />);
219-
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(expected);
219+
expect(renderedComponent.find(NodeWrapper).prop('transform')).toContain(expected);
220220
});
221221

222222
it('applies default zoom level when `zoom` is not specified', () => {
223223
const renderedComponent = shallow(<Tree data={mockData} />);
224-
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(`scale(1)`);
224+
expect(renderedComponent.find(NodeWrapper).prop('transform')).toContain(`scale(1)`);
225225
});
226226

227-
it('respects `scaleExtent` constraints on initital display', () => {
227+
it('respects `scaleExtent` constraints on initial display', () => {
228228
const scaleExtent = { min: 0.2, max: 0.8 };
229229

230230
let renderedComponent = shallow(
231231
<Tree data={mockData} scaleExtent={scaleExtent} zoom={0.9} />,
232232
);
233-
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(
233+
expect(renderedComponent.find(NodeWrapper).prop('transform')).toContain(
234234
`scale(${scaleExtent.max})`,
235235
);
236236

237237
renderedComponent = shallow(<Tree data={mockData} scaleExtent={scaleExtent} zoom={0.1} />);
238-
expect(renderedComponent.find(TransitionGroup).prop('transform')).toContain(
238+
expect(renderedComponent.find(NodeWrapper).prop('transform')).toContain(
239239
`scale(${scaleExtent.min})`,
240240
);
241241
});
@@ -253,6 +253,13 @@ describe('<Tree />', () => {
253253
zoomProps.forEach(nextProps => renderedComponent.setProps(nextProps));
254254
expect(renderedComponent.instance().bindZoomListener).toHaveBeenCalledTimes(4);
255255
});
256+
257+
it('rebinds on `props.transitionDuration` change to handle switched DOM nodes from NodeWrapper', () => {
258+
const renderedComponent = mount(<Tree data={mockData} />);
259+
expect(renderedComponent.instance().bindZoomListener).toHaveBeenCalledTimes(1);
260+
renderedComponent.setProps({ transitionDuration: 0 });
261+
expect(renderedComponent.instance().bindZoomListener).toHaveBeenCalledTimes(2);
262+
});
256263
});
257264

258265
describe('onClick', () => {

0 commit comments

Comments
 (0)