概要
reduxのstore
の内容を使って<canvas>
要素を描画したいと思ったのですが、普通にやってもうまくいかなかったので解決法をメモしておきます。問題の原因は<canvas>
の内容をいじる場合はDOMを直接操作する必要があるので、根本的にreactと相性が悪いことです。とりあえず適当に検索していたら出てきたreactとd3.jsを組み合わせる例の真似をしてみたらうまくいったのですが、もっといい方法がありそうな気がするのでわかる方はぜひコメントください。
結論
<canvas>
の描画は初回のみreactから行い、その後はreactから直接は触らないようにする。そのために、<canvas>
要素を含むReact.Component
において、
-
<canvas>
要素にref
を設定し、 -
componentDidMount()
でcontext
の設定と初回の描画を、 -
shouldComponentUpdate()
でref
から直接DOMを操作して描画内容の更新を行い、false
を返す。
デモ
簡単な例として、フォームに入力したテキストを<canvas>
に画像として描画したい場合を考えます。
コード
コードはgithub (https://github.com/hoture/redux-canvas-example) にあります。以下、簡単に説明します。
まず、以下のようにindex.js
でreducer
とstore
を定義しておきます。state
の構造は非常に単純で、フォームに入力されているテキスト(=<canvas>
に描画したいテキスト)の内容のみを管理します。
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Form, Canvas } from './components'
const initialState = {text: ''}
const reducer = (state = initialState, action) => {
switch (action.type) {
case 'CHANGE_TEXT':
return {
...state,
text: action.text
}
default:
return state
}
}
const store = createStore(reducer)
const render = () => {
ReactDOM.render(
<div>
<Form store={store}/>
<Canvas store={store}/>
</div>,
document.getElementById('root')
)
}
store.subscribe(() => render())
render()
以下、render()
内部で使っている<Form />
と<Canvas />
を定義していきます。まず<Form />
については普通に書けば良いです。
import React, { Component } from 'react'
export const Form = ({ store }) => {
const { text } = store.getState()
return (
<div>
<input
value={text}
onChange={e => store.dispatch({
type: 'CHANGE_TEXT',
text: e.target.value
})}
/>
</div>
)
}
普通です。<input>
のvalue
をstore
からもらってきて表示し、変更が加えられるたびにdispatch
します。
次に、問題になる<canvas>
要素を含む<Canvas />
コンポーネントです。
const canvasWidth = 300
const canvasHeight = 50
export class Canvas extends Component {
componentDidMount() {
this.ctx = this.refs.canvas.getContext('2d')
this.ctx.textAlign = 'center'
this.ctx.font = '24px sans-serif'
const { text } = this.props.store.getState()
this.ctx.fillText(text, canvasWidth / 2, canvasHeight / 2)
}
shouldComponentUpdate() {
const { text } = this.props.store.getState()
this.ctx.clearRect(0, 0, canvasWidth, canvasHeight)
this.ctx.fillText(text, canvasWidth / 2, canvasHeight / 2)
return false
}
render() {
return (
<div>
<canvas ref="canvas" width={canvasWidth} height={canvasHeight} />
</div>
)
}
}
こんな感じで、componentDidMount()
の内部で基本的な設定と描画を行い、shouldComponentUpdate()
の内部でstore
の状態を<canvas>
に反映させることでうまく動きました。このやり方だとreduxのstore
が更新されるたびにshouldComponentUpdate()
が呼ばれその中で直接DOMを操作し、その後false
を返すのでreactはそれ以上<canvas>
を変更しないということが可能になります。