主要的作法有兩種
- 由 React 控制
svg
的 DOM 結構 - 讓
D3
控制svg
的 DOM 結構,但需要實做更新邏輯
許多套件直接使用了這樣的做法,目的只有一個:維持 React 本身 props
與 states
的特性,保有原本 React 的特性,但開發成本極高,以下簡單說明概念
。
class Chart extends Component {
render() {
return (
<svg>
<rect/>
<rect/>
</svg>
)
}
}
在這個狀況下,我們無法在 Component
被 Mount 之後使用 D3 的 Data-driven 機制繪圖,
因為 React 的 DOM 結構邏輯位於 Virtual DOM 中。
此時,原先 D3 的 Data-driven 機制就必須重新實作,等於全部打掉重練。
render() {
return (
<svg>
{this.props.data.map( d=>(<rect/>) )}
</svg>
)
如果想要重新撰寫一套以 React 的機制來實作 D3 的 JSX Component 的話,這的確是一個很棒的作法。
因為以 React 的精神,開發者不需要去管理複雜的 DOM 邏輯,一切 UI 的變化均由 states
決定,而這個 state
就可以對應到餵給圖表的資料。
每種圖表全部打掉重練
GitHub 上已經有許多這樣的專案,但是僅支援少數圖表種類
回歸原先的 D3 並結合 React 是最有彈性的作法,因為只要了解正確觀念,網路上許多基於 D3 開發的套件仍然可以繼續使用, 不需要為了 React 全部打掉。雖然也有相關的 Tools 幫助開發者達到這樣的需求, 但其實原理並不難,你不一定要使用它們,以下簡單說明。
首先,需要對於 React Component 的
生命週期 (Component Specs and Lifecycle) 有基本的認識,
在 React 的世界中,在 componentDidMount
被觸發後,才會產生真正的 DOM。也就是說,
雖然 React 要求使用者在其 Virtual DOM 的架構下思考,但不免仍會有必須操作到真正的 DOM 的情況。
import React, { Component } from 'react'
import { findDOMNode } from 'react-dom'
class Chart extends Component {
componentDidMount(){
var el = findDOMNode(this) // el => <svg></svg>
}
render(){
return <svg/>
}
}
透過 ReactDOM 提供的 findDOMNode
方法,
可以取得最終被 render 出來的 DOM 元素。透過這個方式將 <svg>
取出交給原先的 D3 API 進行繪圖,就可以無痛結合。
...
componentDidMount(){
var el = findDOMNode(this) // el => <svg></svg>
draw(el)
}
render(){
return <svg/>
}
}
// d3 process as you know...
function draw(el){
var svg = d3.select(el)
svg.append('g')
...
}
完整的 Demo 可以參考這個 JSFiddle 上的範例 React D3 - Donut chart
但是,這樣的做法跳過了 Component 原先的 re-render 機制。
也就是說,將圖表資料綁定為 state
後,原先透過 setState
會驅動 render()
的流程,需要額外的控制。
class Chart extends React.Component{
constructor(props) {
super(props)
this.state = {data:getData()}
}
componentDidMount(){
var el = ReactDOM.findDOMNode(this)
draw(el, this.state.data)
}
render(){
return <svg/>
}
}
function getData(){
return [
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100),
Math.floor(Math.random() * 100)
]
}
圖表資料綁定 state
後,一旦 setState
被觸發後,就會重繪 Component。
依照這個思路,只需要將原先在 D3 上更新的動作交給 componentDidUpdate
觸發更新圖表即可。
對 React 來說 Component 只有一個
<svg>
tag,即使改了state
也根本不會有變化
class Chart extends React.Component{
constructor(props) {
super(props)
this.state = {data:getData()}
}
componentDidMount(){
var el = ReactDOM.findDOMNode(this)
draw(el, this.state.data)
}
componentDidUpdate(){
update(this.state.data)
}
render(){
return <svg/>
}
}
透過以上這個概念,就可以無痛將原先撰寫的 D3 圖表整合到 React。
即使使用 Redux 架構也不用擔心,反而更輕鬆。
因為 Redux 幫助開發者將 States 自動綁定到 Props 上,並且偵測更新並重新 assign props
,
因此只需要將範例中的 this.state.data
改為 this.props...
接收新資料並重繪即可。
完整的 Demo 可以參考這個 JSFiddle 上的範例 React D3 - Donut chart (advance)