Edited at

React/Reduxと<canvas>要素を組み合わせて使う

More than 1 year has passed since last update.


概要

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.jsreducerstoreを定義しておきます。stateの構造は非常に単純で、フォームに入力されているテキスト(=<canvas>に描画したいテキスト)の内容のみを管理します。


index.js

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 />については普通に書けば良いです。


components.js

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>valuestoreからもらってきて表示し、変更が加えられるたびにdispatchします。

次に、問題になる<canvas>要素を含む<Canvas />コンポーネントです。


components.jsに追記

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>を変更しないということが可能になります。