refactor: rewrite BaseChart with hooks · open-data-plan/g2plot-react@12d28be · GitHub
Skip to content

Commit 12d28be

Browse files
committed
refactor: rewrite BaseChart with hooks
BREAKING CHANGES - `onMount` has been deprecated, use `ref` instead
1 parent c592af0 commit 12d28be

54 files changed

Lines changed: 668 additions & 406 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎README.md‎

Lines changed: 3 additions & 9 deletions

‎__tests__/plots/__snapshots__/line.spec.tsx.snap‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
exports[`LineChart test lifecycle 1`] = `<div />`;
44

55
exports[`LineChart test lifecycle 2`] = `<div />`;
6+
7+
exports[`LineChart test lifecycle 3`] = `<div />`;

‎__tests__/plots/line.spec.tsx‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,24 @@ describe('LineChart', () => {
1111
ReactDOM.render(<LineChart data={[]} />, div)
1212
})
1313

14-
test('onMount should be called', () => {
15-
const onMount = jest.fn()
14+
test('object ref should be assigned', () => {
15+
const ref = createRef<any>()
1616
const div = document.createElement('div')
17-
ReactDOM.render(<LineChart data={[]} onMount={onMount} />, div)
17+
ReactDOM.render(<LineChart data={[]} ref={ref} />, div)
1818

19-
expect(onMount).toBeCalled()
19+
expect(ref.current).toBeDefined()
20+
})
21+
22+
test('function ref should be called', () => {
23+
// let chart
24+
const getChart = (instance) => {
25+
console.log(instance)
26+
// chart = instance
27+
}
28+
const div = document.createElement('div')
29+
ReactDOM.render(<LineChart data={[]} ref={getChart} />, div)
30+
31+
// expect(chart).toBeDefined()
2032
})
2133

2234
test('state manager', () => {
@@ -110,6 +122,12 @@ describe('LineChart', () => {
110122

111123
expect(instance.props.data).toEqual([{ x: 1 }])
112124

125+
act(() => {
126+
renderer.update(<LineChart data={null} forceFit />)
127+
})
128+
129+
expect(renderer.toJSON()).toMatchSnapshot()
130+
113131
act(() => {
114132
renderer.unmount()
115133
})

‎docs/api/index.md‎

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,18 @@ nav:
66

77
## Configurations
88

9-
All configurations defined in `G2Plot` can be used as `React` `props` in `G2Plot React`
9+
All configurations defined in `G2Plot` can be used as `props`, and use `ref` can access chart instance
1010

1111
For example
1212

13-
```ts
13+
```js
1414
import React from 'react'
1515
import { LineChart } from '@opd/g2plot-react'
1616

17-
export default () => <LineChart xField="x" height={400} forceFit />
17+
export default () => {
18+
const ref = useRef()
19+
return <LineChart xField="x" height={400} ref={ref} forceFit />
20+
}
1821
```
1922

2023
view `G2Plot` [docs](https://g2plot.antv.vision/en/docs/manual/introduction)
21-
22-
## Lifecycle
23-
24-
- `onMount`: `(chart: Plot) => void`
25-
triggered after chart render

‎docs/guide/get-started.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,9 @@ const config = {
107107
}
108108

109109
export default () => {
110-
const handleChartMount = useCallback(chart => {
110+
const getChart = useCallback((chart) => {
111111
console.log(chart)
112112
}, [])
113-
return <LineChart {...config} onMount={handleChartMount} />
113+
return <LineChart {...config} ref={getChart} />
114114
}
115115
```

‎docs/index.md‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ import React, { useCallback } from 'react'
3939
import { LineChart, ColumnChart, PieChart, BarChart } from '@opd/g2plot-react'
4040

4141
const config = {
42-
width: 375,
43-
height: 375,
44-
padding: 'auto',
42+
width: 350,
43+
height: 350,
4544
forceFit: true,
45+
padding: 'auto',
4646
xField: 'year',
4747
yField: 'value',
4848
label: {
@@ -78,8 +78,8 @@ const config = {
7878
}
7979

8080
const pieConfig = {
81-
width: 400,
82-
height: 400,
81+
width: 350,
82+
height: 350,
8383
forceFit: true,
8484
padding: 'auto',
8585
radius: 1,
@@ -121,9 +121,6 @@ const pieConfig = {
121121
}
122122

123123
export default () => {
124-
const handleChartMount = useCallback(chart => {
125-
console.log(chart)
126-
}, [])
127124
return (
128125
<div
129126
style={{
@@ -132,10 +129,15 @@ export default () => {
132129
justifyContent: 'space-between',
133130
}}
134131
>
135-
<LineChart {...config} onMount={handleChartMount} />
136-
<ColumnChart {...config} />
137-
<BarChart {...config} xField="value" yField="year" />
138-
<PieChart {...pieConfig} />
132+
<LineChart style={{ width: '50%', minWidth: 350 }} {...config} />
133+
<ColumnChart style={{ width: '50%', minWidth: 350 }} {...config} />
134+
<BarChart
135+
style={{ width: '50%', minWidth: 350 }}
136+
{...config}
137+
xField="value"
138+
yField="year"
139+
/>
140+
<PieChart style={{ width: '50%', minWidth: 350 }} {...pieConfig} />
139141
</div>
140142
)
141143
}

‎src/components/base/index.tsx‎

Lines changed: 90 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
1-
import React, { HTMLAttributes } from 'react'
2-
import omit from 'lodash/omit'
1+
import React, {
2+
HTMLAttributes,
3+
forwardRef,
4+
useRef,
5+
useContext,
6+
useEffect,
7+
Ref,
8+
MutableRefObject,
9+
ReactElement,
10+
RefAttributes,
11+
} from 'react'
312
import isEqual from 'lodash/isEqual'
413
import cloneDeep from 'lodash/cloneDeep'
514
import {
615
Base as BasePlot,
716
ViewLayer,
817
PlotConfig,
9-
StateManager,
18+
ViewConfig,
1019
} from '@antv/g2plot'
1120
import { StateManagerContext } from '../state-manager'
21+
import { RecursivePartial } from '@antv/g2plot/lib/interface/types'
1222

1323
type PickedAttrs = 'className' | 'style'
1424

@@ -38,87 +48,98 @@ interface StateManagerCfg {
3848
onStateChange?: StateChangeObj[]
3949
}
4050

41-
export interface Plot<C extends PlotConfig = PlotConfig> {
51+
interface ChartConfig extends PlotConfig, ViewConfig {}
52+
53+
export interface Plot<C extends PlotConfig> {
4254
new (container: HTMLElement, props: C): BasePlot<C, LayerCtor<C>>
4355
}
4456

45-
export interface BaseChartProps<C extends PlotConfig = PlotConfig>
57+
const syncRef = <C extends PlotConfig>(
58+
source: MutableRefObject<BasePlot<C, LayerCtor<C>> | null>,
59+
target?: Ref<BasePlot<C, LayerCtor<C>> | null>
60+
) => {
61+
/* istanbul ignore else */
62+
if (typeof target === 'function') {
63+
target(source.current)
64+
} else if (target) {
65+
;(target as MutableRefObject<BasePlot<C, LayerCtor<C>> | null>).current =
66+
source.current
67+
}
68+
}
69+
70+
export interface BaseChartProps<C extends PlotConfig>
4671
extends Pick<HTMLAttributes<HTMLDivElement>, PickedAttrs> {
4772
chart: Plot<C>
48-
onMount?: (chart: BasePlot<C, LayerCtor<C>>) => void
4973
stateManager?: StateManagerCfg
5074
}
5175

52-
export default class BaseChart<
53-
C extends PlotConfig = PlotConfig
54-
> extends React.Component<BaseChartProps<C>> {
55-
private el: HTMLDivElement | null = null
56-
private chart?: BasePlot<C, LayerCtor<C>> | null
57-
private config?: any
58-
private getContainer = (el: HTMLDivElement | null) => {
59-
this.el = el
60-
}
61-
62-
context!: StateManager
63-
64-
private getConfig = (props: BaseChartProps<C>) => {
65-
return omit(props, [
66-
'style',
67-
'className',
68-
'chart',
69-
'onMount',
70-
'stateManager',
71-
]) as C
72-
}
73-
74-
static contextType = StateManagerContext
75-
76-
componentDidMount() {
77-
const { chart, onMount, stateManager } = this.props
78-
const config = this.getConfig(this.props)
79-
const Chart = chart
80-
const { data, ...restConfig } = config as any
81-
this.config = cloneDeep(restConfig)
82-
if (this.el) {
83-
this.chart = new Chart(this.el, config)
84-
this.chart.render()
85-
if (typeof onMount === 'function') {
86-
onMount(this.chart)
76+
const BaseChart = <C extends PlotConfig>(
77+
props: BaseChartProps<C>,
78+
ref?: Ref<BasePlot<C, LayerCtor<C>> | null>
79+
) => {
80+
const {
81+
chart: Chart,
82+
stateManager: stateManagerCfg,
83+
style,
84+
className,
85+
...restProps
86+
} = props
87+
const chartRef = useRef<BasePlot<C, LayerCtor<C>> | null>(null)
88+
const configRef = useRef<ChartConfig>()
89+
const containerRef = useRef<HTMLDivElement>(null)
90+
const stateManager = useContext(StateManagerContext)
91+
const isFirstRenderRef = useRef<boolean>(true)
92+
93+
useEffect(() => {
94+
const { current: container } = containerRef
95+
/* istanbul ignore else */
96+
if (container) {
97+
const { data, ...config } = restProps as ChartConfig
98+
configRef.current = cloneDeep(config)
99+
chartRef.current = new Chart(container, restProps as C)
100+
chartRef.current.render()
101+
/* istanbul ignore else */
102+
if (stateManager && stateManagerCfg) {
103+
chartRef.current.bindStateManager(stateManager, stateManagerCfg)
87104
}
88-
89-
if (this.context && stateManager) {
90-
this.chart.bindStateManager(this.context, stateManager)
105+
}
106+
syncRef(chartRef, ref)
107+
return () => {
108+
/* istanbul ignore else */
109+
if (chartRef.current) {
110+
chartRef.current.destroy()
111+
chartRef.current = null
112+
syncRef(chartRef, ref)
91113
}
92114
}
93-
}
94-
95-
componentDidUpdate() {
96-
const config = this.getConfig(this.props)
97-
const { data, ...restConfig } = config as any
98-
const isConfigChanged = !isEqual(this.config, restConfig)
115+
// eslint-disable-next-line
116+
}, [])
99117

118+
useEffect(() => {
119+
const { current: chart } = chartRef
100120
/* istanbul ignore else */
101-
if (this.chart) {
102-
if (isConfigChanged) {
103-
this.config = cloneDeep(restConfig)
104-
this.chart.updateConfig(config as any)
105-
this.chart.render()
121+
if (chart) {
122+
// avoid update in first time
123+
if (!isFirstRenderRef.current) {
124+
const { data, ...config } = restProps as ChartConfig
125+
if (!isEqual(config, configRef.current)) {
126+
chart.updateConfig(config as RecursivePartial<C>)
127+
} else {
128+
if (data) {
129+
chart.changeData(data)
130+
} else {
131+
chart.changeData([])
132+
}
133+
}
106134
} else {
107-
this.chart.changeData(data)
135+
isFirstRenderRef.current = false
108136
}
109137
}
110-
}
111-
112-
componentWillUnmount() {
113-
/* istanbul ignore else */
114-
if (this.chart) {
115-
this.chart.destroy()
116-
this.chart = null
117-
}
118-
}
138+
}, [restProps])
119139

120-
render() {
121-
const { style, className } = this.props
122-
return <div ref={this.getContainer} className={className} style={style} />
123-
}
140+
return <div style={style} className={className} ref={containerRef} />
124141
}
142+
143+
export default forwardRef(BaseChart) as <C extends PlotConfig>(
144+
p: BaseChartProps<C> & RefAttributes<BasePlot<C, LayerCtor<C>> | null>
145+
) => ReactElement

‎src/plots/area/index.tsx‎

Lines changed: 9 additions & 6 deletions

0 commit comments

Comments
 (0)