germ13
germ13

Reputation: 531

Rendering / Returning HTML5 Canvas in ReactJS

I am trying to render a canvas using React. The graphics inside the canvas would also be handled by Reacts render method.

The current code does render a canvas with the graphics I want. But I am looking for a way to return a canvas with my graphics from within the render method. The reason I want this, is cause I want this to work in "React Way".

Ideally, I would want my code to be something like this:

return(
  <div>
    <canvas id='my_canvas_with_graphics_rendered_in_react'></canvas>
  </div>
);

First, I want to know if this is even possible. Or if I should look for an alternative.

Maybe Im missing the point of React, if that is the case please let me know. I do hope there is a solution, because from what I've read React touts itself as the V in MVC. That is why, if possible, this would be a perfect solution for what I'm trying to do. I would not have to worry about the rendering inside my canvas. I would simply supply the data to the component and React would re-render the changes.

I have marked of in the return statement where I believe the correct code should go.

The HTML code:

<div id='wrapper'>
    <canvas id='canvas'></canvas>
</div>

The jsx code:

var MyCanvas = React.createClass({
    render:function(){
        var line = this.props.lines;
        var ctx = this.props.canvas.getContext("2d");

        ctx.strokeStyle = 'rgba(0,0,0,0.5)';
        ctx.beginPath();
        ctx.moveTo(line[0].x0, line[0].y0);
        // .... more code .....
        ctx.stroke();

        return(
            <div>
                /* *********************************
                ??? RETURN MY RENDERED CANVAS
                    WITH GRAPHIC LINES. ????
             This is where I want to return the canvas 
                *********************************  */
            </div>
        );

    }
});


var wrapper = document.getElementById('wrapper');
var canvas = document.getElementById('canvas');
var line1 = { x0: 0, y0: 10, x1: 20, y1: 30 };
var line2 = { x0: 40, y0: 50, x1: 60, y1: 70 };

var myCanvas = <MyCanvas canvas={ canvas } lines={ [line1, line2] } />;
React.render(myCanvas, wrapper);

Hope I made myself clear.

Upvotes: 26

Views: 30474

Answers (5)

Souradeep Nanda
Souradeep Nanda

Reputation: 3288

I ported @matthew answer to use React Hooks. React changes so frequently :)

import React, { useEffect } from 'react';
import PropTypes from 'prop-types';

const Canvas = ({ draw }) => {
  const canvas = React.createRef();

  useEffect(() => {
    const ctx = canvas.current.getContext('2d');
    draw(ctx);
  }, [draw, canvas]);

  return <canvas ref={canvas} />;
};

Canvas.propTypes = {
  draw: PropTypes.func.isRequired,
};

export default Canvas;

You would use this Canvas by supplying the draw context.

<Canvas  
  draw={ctx => {
    ctx.beginPath();
    ctx.arc(95, 50, 40, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.stroke();
    }
  }
/>

Upvotes: 4

Matt
Matt

Reputation: 1558

I thought I would update this answer using the React.createRef() because the this.refs is now legacy. Refs and the DOM

import React from "react";
import * as PropTypes from "prop-types";

class Canvas extends React.Component {
    constructor() {
        super()
        this.canvas = React.createRef()
    }
    componentDidMount() {
        const ctx = this.canvas.current.getContext('2d')
        this.props.draw(ctx);
    }
    render() {
        return <canvas ref={this.canvas} />;        
    }
}
Canvas.propTypes = {
  draw: PropTypes.func.isRequired
};
export default Canvas

You would use this Canvas by supplying the draw context.

<Canvas  
  draw={ctx => {
    ctx.beginPath();
    ctx.arc(95, 50, 40, 0, 2 * Math.PI);
    ctx.closePath();
    ctx.stroke();
    }
  }
/>

Upvotes: 8

RedMatt
RedMatt

Reputation: 545

The key is overriding the right React lifecycle methods to do the drawing. The render method creates the canvas DOM element and you can add a refs callback to set a reference to it, but you have to wait until the component is mounted to draw on the canvas.

Here's an example that uses a callback ref as the React docs recommend. It is designed to render a static (non-animated) graphic component based on props and updates when props change. It also subscribes to the window resize event so that it can be dynamically sized via CSS (such as width: 100%). This example is based on the code from this Gist.

export default class CanvasComponent extends React.Component {

    constructor(props) {
        super(props);

        this._resizeHandler = () => {
            /* Allows CSS to determine size of canvas */
            this.canvas.width = this.canvas.clientWidth;
            this.canvas.height = this.canvas.clientHeight;

            this.clearAndDraw();
        }
    }

    componentDidMount() {
        window.addEventListener('resize', this._resizeHandler);

        /* Allows CSS to determine size of canvas */
        this.canvas.width = this.canvas.clientWidth;
        this.canvas.height = this.canvas.clientHeight;

        this.clearAndDraw();
    }

    componentWillUnmount() {
        window.removeEventListener('resize', this._resizeHandler);
    }

    componentDidUpdate(prevProps, prevState) {
        if (this.props.secondRect !== prevProps.secondRect) {
            this.clearAndDraw();
        }
    }

    clearAndDraw() {
        const ctx = this.canvas.getContext('2d');
        if (ctx) {
            ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
            this.draw(ctx);
        }
    }

    draw(ctx) {
        ctx.fillStyle = 'rgb(200, 0, 0)';
        ctx.fillRect(10, 10, 50, 50);

        if (this.props.secondRect) {
            ctx.fillStyle = 'rgba(0, 0, 200, 0.5)';
            ctx.fillRect(30, 30, 50, 50);
        }
    }

    render() {
        return (
            <canvas ref={canvas => this.canvas = canvas} />
        );
    }
}

You can set the secondRect prop from the parent component or set it from state to see the component update when the prop updates. You can extend this idea to build any kind of data-driven canvas rendering, such as a chart or graph.

Upvotes: 9

Aligertor
Aligertor

Reputation: 643

Your only error was that you were not familar with react refs. try this:

class ConnectionChart extends React.Component {

    componentDidMount() {
        let canvas = ReactDOM.findDOMNode(this.refs.myCanvas);
        let ctx = canvas.getContext('2d');

        ctx.fillStyle = 'rgb(200,0,0)';
        ctx.fillRect(10, 10, 55, 50);
    }

    render() {
        return (
            <div>
                <canvas ref="myCanvas" />
            </div>
        );
    }
}

You might have get rid of the ES6 style but you get the idea. Of cause u can paint in other methods too ^^

Upvotes: 21

Anders Ekdahl
Anders Ekdahl

Reputation: 22933

Instead of using the canvas in your render method, you'd do something like this:

var MyCanvas = React.createClass({
  componentDidMount: function () {
    React.getDOMNode(this).appendChild(this.props.canvas);
  },
  render: function() {
    return <div />;
  }
});

You just render an empty div and wait for React to mount that. When mounted, you append your canvas to the actual DOM node that React created.

The reason for this is that the render method only returns virtual DOM nodes that React then translates to real DOM nodes. And since you have a real DOM node, you can't convert that to a virtual node. Another reason for is that you should only return nodes from render that React controls. And since you're managing the canvas from outside of Reacts control, you should use real DOM apis do manage it.

Upvotes: 13

Related Questions