Skip to content

Commit 1abc1b3

Browse files
committed
fix(compat): re-establish React 15 support
refactor(compat): Node - move from componentWillUpdate to componentDidUpdate refactor(compat): removes useless state in NodeWrapper * refactor to FunctionComponent refactor(compat): Tree - migrate to React 16 lifecycle methods * polyfill React 16 lifecycle methods for React 15 support chore: remove unneeded parallel lockfile
1 parent 8eb2fab commit 1abc1b3

File tree

9 files changed

+78
-13601
lines changed

9 files changed

+78
-13601
lines changed

package-lock.json

Lines changed: 0 additions & 13492 deletions
This file was deleted.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
"d3": "3.5.17",
6666
"deep-equal": "^1.0.1",
6767
"prop-types": "^15.5.10",
68+
"react-lifecycles-compat": "^3.0.4",
6869
"react-transition-group": "^1.1.3",
6970
"uuid": "^3.0.1"
7071
},

src/Node/index.js

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,11 @@ export default class Node extends React.Component {
1515
};
1616

1717
componentDidMount() {
18-
const { nodeData, orientation, transitionDuration } = this.props;
19-
const transform = this.setTransform(nodeData, orientation);
20-
21-
this.applyTransform(transform, transitionDuration);
18+
this.commitTransform();
2219
}
2320

24-
// eslint-disable-next-line camelcase
25-
UNSAFE_componentWillUpdate(nextProps) {
26-
const transform = this.setTransform(nextProps.nodeData, nextProps.orientation);
27-
this.applyTransform(transform, nextProps.transitionDuration);
21+
componentDidUpdate() {
22+
this.commitTransform();
2823
}
2924

3025
shouldComponentUpdate(nextProps) {
@@ -66,6 +61,13 @@ export default class Node extends React.Component {
6661
}
6762
}
6863

64+
commitTransform() {
65+
const { nodeData, orientation, transitionDuration } = this.props;
66+
const transform = this.setTransform(nodeData, orientation);
67+
68+
this.applyTransform(transform, transitionDuration);
69+
}
70+
6971
renderNodeElement = nodeStyle => {
7072
const { circleRadius, nodeSvgShape } = this.props;
7173
/* TODO: DEPRECATE <circle /> */

src/Tree/NodeWrapper.js

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,40 +2,21 @@ import React from 'react';
22
import T from 'prop-types';
33
import { TransitionGroup } from 'react-transition-group';
44

5-
export default class NodeWrapper extends React.Component {
6-
state = {
7-
enableTransitions: this.props.transitionDuration > 0,
8-
};
9-
10-
// eslint-disable-next-line camelcase
11-
UNSAFE_componentWillReceiveProps(nextProps) {
12-
if (nextProps.transitionDuration !== this.props.transitionDuration) {
13-
this.setState({
14-
enableTransitions: nextProps.transitionDuration > 0,
15-
});
16-
}
17-
}
18-
19-
render() {
20-
if (this.state.enableTransitions) {
21-
return (
22-
<TransitionGroup
23-
component={this.props.component}
24-
className={this.props.className}
25-
transform={this.props.transform}
26-
>
27-
{this.props.children}
28-
</TransitionGroup>
29-
);
30-
}
31-
32-
return (
33-
<g className={this.props.className} transform={this.props.transform}>
34-
{this.props.children}
35-
</g>
36-
);
37-
}
38-
}
5+
// eslint-disable-next-line
6+
const NodeWrapper = props =>
7+
props.transitionDuration > 0 ? (
8+
<TransitionGroup
9+
component={props.component}
10+
className={props.className}
11+
transform={props.transform}
12+
>
13+
{props.children}
14+
</TransitionGroup>
15+
) : (
16+
<g className={props.className} transform={props.transform}>
17+
{props.children}
18+
</g>
19+
);
3920

4021
NodeWrapper.defaultProps = {
4122
component: 'g',
@@ -48,3 +29,5 @@ NodeWrapper.propTypes = {
4829
transform: T.string.isRequired,
4930
children: T.array.isRequired,
5031
};
32+
33+
export default NodeWrapper;

src/Tree/index.js

Lines changed: 35 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import React from 'react';
2+
import { polyfill } from 'react-lifecycles-compat';
23
import T from 'prop-types';
34
import { layout, select, behavior, event } from 'd3';
45
import clone from 'clone';
@@ -10,9 +11,11 @@ import Node from '../Node';
1011
import Link from '../Link';
1112
import './style.css';
1213

13-
export default class Tree extends React.Component {
14+
class Tree extends React.Component {
1415
state = {
15-
data: this.assignInternalProperties(clone(this.props.data)),
16+
// eslint-disable-next-line react/no-unused-state
17+
dataRef: this.props.data,
18+
data: Tree.assignInternalProperties(clone(this.props.data)),
1619
rd3tSvgClassName: `_${uuid.v4()}`,
1720
rd3tGClassName: `_${uuid.v4()}`,
1821
};
@@ -27,6 +30,19 @@ export default class Tree extends React.Component {
2730
},
2831
};
2932

33+
static getDerivedStateFromProps(nextProps, prevState) {
34+
// Clone new data & assign internal properties if `data` object reference changed.
35+
if (nextProps.data !== prevState.dataRef) {
36+
return {
37+
// eslint-disable-next-line react/no-unused-state
38+
dataRef: nextProps.data,
39+
data: Tree.assignInternalProperties(clone(nextProps.data)),
40+
};
41+
}
42+
43+
return null;
44+
}
45+
3046
constructor(props) {
3147
super(props);
3248
this.internalState.d3 = Tree.calculateD3Geometry(this.props);
@@ -38,8 +54,14 @@ export default class Tree extends React.Component {
3854
}
3955

4056
componentDidUpdate(prevProps) {
41-
// Rebind zoom listeners to new DOM nodes in case NodeWrapper switched <TransitionGroup> <-> <g>
42-
if (prevProps.transitionDuration !== this.props.transitionDuration) {
57+
// If zoom-specific props change -> rebind listener with new values
58+
// Or: rebind zoom listeners to new DOM nodes in case NodeWrapper switched <TransitionGroup> <-> <g>
59+
if (
60+
!deepEqual(this.props.translate, prevProps.translate) ||
61+
!deepEqual(this.props.scaleExtent, prevProps.scaleExtent) ||
62+
this.props.zoom !== prevProps.zoom ||
63+
this.props.transitionDuration !== prevProps.transitionDuration
64+
) {
4365
this.bindZoomListener(this.props);
4466
}
4567

@@ -50,31 +72,11 @@ export default class Tree extends React.Component {
5072
translate: this.internalState.d3.translate,
5173
});
5274

75+
this.internalState.d3 = Tree.calculateD3Geometry(this.props);
5376
this.internalState.targetNode = null;
5477
}
5578
}
5679

57-
// eslint-disable-next-line camelcase
58-
UNSAFE_componentWillReceiveProps(nextProps) {
59-
// Clone new data & assign internal properties
60-
if (this.props.data !== nextProps.data) {
61-
this.setState({
62-
data: this.assignInternalProperties(clone(nextProps.data)),
63-
});
64-
}
65-
66-
this.internalState.d3 = Tree.calculateD3Geometry(nextProps);
67-
68-
// If zoom-specific props change -> rebind listener with new values
69-
if (
70-
!deepEqual(this.props.translate, nextProps.translate) ||
71-
!deepEqual(this.props.scaleExtent, nextProps.scaleExtent) ||
72-
this.props.zoom !== nextProps.zoom
73-
) {
74-
this.bindZoomListener(nextProps);
75-
}
76-
}
77-
7880
/**
7981
* setInitialTreeDepth - Description
8082
*
@@ -134,11 +136,12 @@ export default class Tree extends React.Component {
134136
* `data` set that are required for tree manipulation and returns
135137
* a new `data` array.
136138
*
139+
* @static
137140
* @param {array} data Hierarchical tree data
138141
*
139142
* @return {array} `data` array with internal properties added
140143
*/
141-
assignInternalProperties(data) {
144+
static assignInternalProperties(data) {
142145
// Wrap the root node into an array for recursive transformations if it wasn't in one already.
143146
const d = Array.isArray(data) ? data : [data];
144147
return d.map(node => {
@@ -149,7 +152,7 @@ export default class Tree extends React.Component {
149152
}
150153
// If there are children, recursively assign properties to them too
151154
if (node.children && node.children.length > 0) {
152-
node.children = this.assignInternalProperties(node.children);
155+
node.children = Tree.assignInternalProperties(node.children);
153156
node._children = node.children;
154157
}
155158
return node;
@@ -500,7 +503,6 @@ export default class Tree extends React.Component {
500503
} = this.props;
501504
const { translate, scale } = this.internalState.d3;
502505
const subscriptions = { ...nodeSize, ...separation, depthFactor, initialDepth };
503-
504506
return (
505507
<div className={`rd3t-tree-container ${zoomable ? 'rd3t-grabbable' : undefined}`}>
506508
<svg className={rd3tSvgClassName} width="100%" height="100%">
@@ -640,3 +642,8 @@ Tree.propTypes = {
640642
links: T.object,
641643
}),
642644
};
645+
646+
// Polyfill React 16 lifecycle methods for compat with React 15.
647+
polyfill(Tree);
648+
649+
export default Tree;

src/Tree/tests/NodeWrapper.test.js

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,11 @@
11
import React from 'react';
2-
import { shallow, mount } from 'enzyme';
2+
import { shallow } from 'enzyme';
33
import { TransitionGroup } from 'react-transition-group';
44

55
import NodeWrapper from '../NodeWrapper';
66

77
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', () => {
8+
it('renders a <g> when transitions are disabled', () => {
149
const fixture = {
1510
transform: 't',
1611
className: 'cls',
@@ -22,7 +17,7 @@ describe('<NodeWrapper />', () => {
2217
expect(renderedComponent.find('g').prop('className')).toContain(fixture.className);
2318
});
2419

25-
it('Renders a <TransitionGroup> when transitions are enabled', () => {
20+
it('renders a <TransitionGroup> when transitions are enabled', () => {
2621
const fixture = {
2722
component: 'g',
2823
transform: 't',
@@ -35,21 +30,4 @@ describe('<NodeWrapper />', () => {
3530
expect(renderedComponent.find(TransitionGroup).prop('className')).toContain(fixture.className);
3631
expect(renderedComponent.find(TransitionGroup).prop('component')).toContain(fixture.component);
3732
});
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-
});
5533
});

src/Tree/tests/index.test.js

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { mockData, mockData2, mockData3, mockData4 } from './mockData';
1010

1111
describe('<Tree />', () => {
1212
jest.spyOn(Tree.prototype, 'generateTree');
13-
jest.spyOn(Tree.prototype, 'assignInternalProperties');
13+
jest.spyOn(Tree, 'assignInternalProperties');
1414
jest.spyOn(Tree, 'collapseNode');
1515
jest.spyOn(Tree, 'expandNode');
1616
jest.spyOn(Tree.prototype, 'setInitialTreeDepth');
@@ -86,13 +86,9 @@ describe('<Tree />', () => {
8686
data: mockData2,
8787
};
8888
const renderedComponent = mount(<Tree data={mockData} />);
89-
expect(renderedComponent.instance().assignInternalProperties).toHaveBeenCalledTimes(
90-
mockDataDepth,
91-
);
89+
expect(Tree.assignInternalProperties).toHaveBeenCalledTimes(mockDataDepth);
9290
renderedComponent.setProps(nextProps);
93-
expect(renderedComponent.instance().assignInternalProperties).toHaveBeenCalledTimes(
94-
mockDataDepth + mockData2Depth,
95-
);
91+
expect(Tree.assignInternalProperties).toHaveBeenCalledTimes(mockDataDepth + mockData2Depth);
9692
});
9793

9894
it("reassigns internal props if `props.data`'s array reference changes", () => {
@@ -102,13 +98,9 @@ describe('<Tree />', () => {
10298
const nextData = [...mockData];
10399
nextData[0].children.push({ name: `${nextData[0].children.length}` });
104100
const renderedComponent = mount(<Tree data={mockData} />);
105-
expect(renderedComponent.instance().assignInternalProperties).toHaveBeenCalledTimes(
106-
mockDataDepth,
107-
);
101+
expect(Tree.assignInternalProperties).toHaveBeenCalledTimes(mockDataDepth);
108102
renderedComponent.setProps({ data: nextData });
109-
expect(renderedComponent.instance().assignInternalProperties).toHaveBeenCalledTimes(
110-
mockDataDepth + nextDataDepth,
111-
);
103+
expect(Tree.assignInternalProperties).toHaveBeenCalledTimes(mockDataDepth + nextDataDepth);
112104
});
113105

114106
describe('translate', () => {

webpack.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* global __dirname, require, module*/
1+
/* global __dirname, require, module */
22

33
const webpack = require('webpack');
44
const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;
@@ -33,6 +33,7 @@ const config = {
3333
'd3',
3434
'react',
3535
'react-transition-group',
36+
'react-lifecycles-compat',
3637
'prop-types',
3738
'uuid',
3839
'deep-equal',

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5564,6 +5564,11 @@ react-is@^16.6.1, react-is@^16.6.3:
55645564
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
55655565
integrity sha512-u7FDWtthB4rWibG/+mFbVd5FvdI20yde86qKGx4lVUTWmPlSWQ4QxbBIrrs+HnXGbxOUlUzTAP/VDmvCwaP2yA==
55665566

5567+
react-lifecycles-compat@^3.0.4:
5568+
version "3.0.4"
5569+
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
5570+
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
5571+
55675572
react-test-renderer@^16.0.0-0:
55685573
version "16.1.1"
55695574
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-16.1.1.tgz#a05184688d564be799f212449262525d1e350537"

0 commit comments

Comments
 (0)