Help us understand the problem. What is going on with this article?

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>に画像として描画したい場合を考えます。

redux-canvas.gif

コード

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away