2
\$\begingroup\$

I wrote some JavaScript to generate some graphics on a canvas, on a regular HTML page.

I now want the same code to run in a component that is part of a React app. I've done this:

  componentDidMount() {
    const canvas = this.refs.firstCanvas
    const ctx = canvas.getContext('2d')
    const bgCanvas = this.refs.firstbgCanvas
    const bgCtx = bgCanvas.getContext('2d')

    function generateStarfield() {
      bgCtx.clearRect(0, 0, bgCanvas.width, bgCanvas.height)
      for (let i = 0; i < 2000; i++) {
        bgCtx.beginPath()
        const x = Math.random()*bgCanvas.width
        const y = Math.random()*bgCanvas.height
        bgCtx.arc(x, y, 0.35, 0, 2*Math.PI, 'anticlockwise')
        bgCtx.closePath()
        bgCtx.fillStyle = 'white'
        bgCtx.fill()
      }
    }
    generateStarfield()

    const origin_x = 1000
    const origin_y = 1000
    const scale = 1000

    class planet {
      constructor(orbital_velocity, offset_theta, orbital_radius, radius, colour) {
        this.orbital_velocity = orbital_velocity
        this.orbital_radius = orbital_radius
        this.colour = colour
        this.radius = radius
        this.offset_theta = offset_theta
        this.draw()
      }

      draw() {
      const theta = this.offset_theta + (t*this.orbital_velocity)
      const x = origin_x + (this.orbital_radius*Math.cos(theta))
      const y = origin_y + (this.orbital_radius*Math.sin(theta))
      ctx.beginPath()
      ctx.arc(x, y, this.radius, 0, 2*Math.PI, 'anticlockwise')
      ctx.closePath()
      ctx.fillStyle = this.colour
      ctx.fill()
      }
    }


    let t = 0

    const a = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.1, 10, 'white')
    const b = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.2, 16, 'white')
    const c = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.3, 18, 'white')
    const d = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.4, 14, 'white')
    const e = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.5, 12, 'white')
    const f = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.6, 28, 'white')
    const g = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.7, 22, 'white')
    const h = new planet((0.2+0.4*Math.random())*Math.PI, 2*Math.random()*Math.PI, scale*0.8, 20, 'white')

    setInterval(function() {
      ctx.clearRect(0, 0, canvas.width, canvas.height)
      ctx.beginPath()
      a.draw()
      b.draw()
      c.draw()
      d.draw()
      e.draw()
      f.draw()
      g.draw()
      h.draw()
      t += 0.05
    }, 40)
  }

Now, this works (!), but I've just lifted it directly from the original JavaScript, and modified the document.getElementByID calls to use refs. Is it bad practice to stick all of this stuff into componentDidMount?

Note - I'm aware that the code itself could be tidied up, I'm mainly asking about putting all of this (especially the definitions for planet and generateStarField) into componentDidMount.

\$\endgroup\$
1
\$\begingroup\$

You can do this, if it works... but it's not really using the React architecture or patterns.

  • Most of this code that is drawing should be in the render method.

  • You'll want to move some of the state (canvas, ctx, bgCanvas, bgCtx) into the class's state, or just as instance variables.

  • You can probably do most of the creation in the constructor, although doing it in componentDidMount will work.

  • You can integrate the animation with React, setInterval(() => this.forceUpdate(() => this.t += 0.05)). This makes sense in componentDidMount. You'll also want to turn this off in the unmount method.

EDIT/ADDED LATER

...maybe you just want to leave it the way it is, although it's quite non-React... it's working fine, though, and there aren't really problems...

To make it "React" you're probably going to have to re-write it completely-- because just moving pieces around like I suggested may make you end up fighting the framework more than leveraging. Looking more, you'd probably make separate components for the different canvases and perhaps pass in t as a property, so there's a container component that functions as the "clock". Doing something like that would allow the regular React rendering mechanisms work.

\$\endgroup\$
  • \$\begingroup\$ I'm a bit confused about the order of this - the canvas, ctx, bgCanvas, and bgCtx can't be defined until after render has completed (as render creates the <canvas /> elements), so how can any drawing be done until the componentDidMount? \$\endgroup\$ – Alex Jun 1 at 17:10
  • \$\begingroup\$ Yeah, there's a bit of a start-up ordering issue, but I think having React.createRef() in the constructor and then you can just check in the render if it's set will work... \$\endgroup\$ – ndp Jun 1 at 21:04
  • \$\begingroup\$ No you should not render animated content via setInterval and worse as canvas context is completely independent of the DOM you should never animate its content via reacts forceUpdate. Should use requestAnimationFrame in componentDidMount and stop it in componentWillUnmount() \$\endgroup\$ – Blindman67 Jun 2 at 3:47

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.