React para principiantes. Como usar el elemento de canvas

Las mejores practicas; con React Hooks y rendimiento

Logo de React en un lienzo

Logo de React en un lienzo

Introducción

Normalmente, cuando usamos vanilla JavaScript para manipular el elemento de canvas, tenemos que obtener una referencia del HTMLElement del canvas en el DOM, y después ejecutar HTMLCanvasElement.getContext() para obtener un contexto de dibujo y así empezar a usar el canvas. Sin embargo, en React, utilizamos JSX para manipular el DOM, y cuando necesitamos acceso a la referencia HTMLElement, simplemente se utiliza el hook useRef de React. No obstante, puede ser confuso saber en donde llamar a useRef y getContext de tal manera que solo sean ejecutados cuando sea necesario, y así evitar cálculos innecesarios en cada renderizado.

Al Punto

1. Crear una nueva app de React

Crea una nueva app de React, y una vez que todos los paquetes se instalen, cambia el directo a la carpeta recién creada

npx create-react-app canvas
cd canvas

Finalmente, puedes ejecutar el comando de inicio y verás un nuevo proyecto en localhost:3000

npm run start

Nuevo proyecto en locahost. Logo de React

Pequeño consejo

Después de instalar las dependencias de create-react-app, obtuve la siguiente advertencia de npm audit, a pesar de que acabo de inicializar la app.

27 vulnerabilities (16 moderate, 9 high, 2 critical)

Aunque npm audit no es la mejor herramienta para examinar riesgos en la seguridad de tu app, si no quieres ver la advertencia, puedes mover react-scripts de dependencies a devDependencies en package.json. No obstante, npm audit aún te advierte por vulnerabilidades en las dependencias de desarrollo, por lo que tendrás que ejecutar npm audit --production para no ver la advertencia.

// canvas/package.json "dependencies": { "@testing-library/jest-dom": "^5.15.0", "@testing-library/react": "^11.2.7", "@testing-library/user-event": "^12.8.3", "react": "^17.0.2", "react-dom": "^17.0.2", "web-vitals": "^1.1.2", }, "devDependencies": { "react-scripts": "4.0.3" },

2. Crea y referencia el elemento de Canvas

Una vez que todo este listo, puedes abrir el archivo src/App.js y borrar todo el código de muestra, dejando solamente un componente funcional. Posteriormente, añade un elemento de canvas dentro, importa el hook de useRef de react, y crea una referencia al canvas a través del atributo ref.

// canvas/src/App.js import {useRef} from "react"; function App() { const canvasRef = useRef(null); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Nota

Pasamos null como el primero argumento de useRef para utilizarlo como el valor inicial de canvasRef, que es la variable que almacenará el HTMLElement del canvas.

3. Crea un contexto

Puedes crear un contexto de dibujo que este globalmente disponible en el componente al llamar getContext al inicio de la función. Después pasamos "2d" como el primer parámetro de getContext ya que este define el tipo de contexto del canvas, el cual puede estar en dos o tres dimensiones.

// canvas/src/App.js import {useRef} from "react"; function App() { const canvasRef = useRef(null); const canvas = canvasRef.current; const context = canvas.getContext("2d"); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Nota

La referencia del canvas no es almacenado directamente en el canvasRef, sino en su única propiedad llamada .current.

Sin embargo, esta implementación no es la más apropiada, ya que cada vez que el compone sea renderizado, getContext será llamada. No obstante, de acuerdo a la MDN,

Ejecuciones posteriores del método getContext del mismo elemento canvas, con el mismo argumento de tipo de contexto, siempre devolverán el mismo contexto de dibujo como fue devuelto cuando el método fue invocado por primera vez. No es posible obtener otro contexto de dibujo de un mismo elemento canvas.

Por lo que no te tienes que preocupar por tu app rompiéndose por llamar varias veces al método getContext. No obstante, deberías evitar cálculos innecesarios al crear el contexto dentro del hook `useEffect

. Vale la pena decir que no tenemos que añadir canvasRef dentro del arreglo de dependencias deuseEffect`, ya que mutar una referencia no provoca una nueva renderización, así que dejamos el arreglo vacío, y por ende sólo se ejecuta una vez.

// canvas/src/App.js import {useRef, useEffect} from "react"; function App() { const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); }, []); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Sin embargo, ahora el contexto no está globalmente disponible; solamente existe en el interior de useEffect, así que para arreglarlo tendrás que crear un estado global con el hook useState y con null como su valor inicial. Después, dentro de useEffect, asignar el contexto a dicho estado.

// canvas/src/App.js import {useRef, useState, useEffect} from "react"; function App() { const [canvasContext, setCanvasContext] = useState(null); const canvasRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; const context = canvas.getContext("2d"); setCanvasContext(context); }, []); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

4. Dibuja algo!

Finalmente, manipularemos el contexto y el elemento canvas al redimensionar el canvas al tamaño de la ventana y cambiar el color del fondo cada vez que el usuario haga click en el canvas.

Si inspeccionas el elemento canvas con las dev tools, verás que el canvas es sólo un pequeño rectángulo en una esquina.

Elemento canva que ocupa una pequeña parte de la ventana

Para redimensionarlo, puedes acceder a la propiedad del objeto global window para obtener sus propiedades de grosor y largo, y después utilizar canvasRef.current para cambiar el tamaño del canvas.

// canvas/src/App.js import {useRef, useState, useEffect} from "react"; function App() { const [canvasContext, setCanvasContext] = useState(null); const canvasRef = useRef(null); useEffect(() => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const canvas = canvasRef.current; canvas.width = windowWidth; canvas.height = windowHeight; const context = canvas.getContext("2d"); setCanvasContext(context); }, [canvasRef]); return ( <div> <canvas ref={canvasRef}></canvas> </div> ); } export default App;

Para finalizar, puedes utilizar el contexto para manipular el contenido del canvas dentro su atributo onClick. Ahí, puedes usar el método context.fillStyle para cambiar el color del rectángulo, y después utilizar el método context.fillRect para dibujar el rectángulo en sí. context.fillRect toma 4 argumentos, los dos primeros son las coordenadas iniciales x, y (que serán 0,0) y los dos últimos son las coordenadas finales (que serán el grosor y altura del canvas).

// canvas/src/App.js //.... return ( <div> <canvas ref={canvasRef} onClick={() => { canvasContext.fillStyle = "red"; canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); }}></canvas> </div> ); //....

De esta manera, si haces click en el elemento de canvas, verás que va cambiar de blanco a rojo. Ahora puedes añadir un arreglo con los nombres de colores, y utilizar Math.random para seleccionar aleatoriamente un elemento cada vez que hay un click.

// canvas/src/App.js import {useRef, useState, useEffect} from "react"; const colores = ["red", "green", "blue", "yellow", "purple", "orange", "black", "white", "brown"]; const getRandomColor = () => { const randomIndex = Math.floor(Math.random() * colores.length); return colores[randomIndex]; }; function App() { const [canvasContext, setCanvasContext] = useState(null); const canvasRef = useRef(null); useEffect(() => { const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; const canvas = canvasRef.current; canvas.width = windowWidth; canvas.height = windowHeight; const context = canvas.getContext("2d"); setCanvasContext(context); }, [canvasRef]); return ( <div> <canvas ref={canvasRef} onClick={() => { canvasContext.fillStyle = getRandomColor(); canvasContext.fillRect(0, 0, canvasContext.canvas.width, canvasContext.canvas.height); }}></canvas> </div> ); } export default App;

Nota

Math.random devuelve un valor aleatorio entre 0 y 1, por lo que lo multiplicamos por el largo del arreglo de colores.

Resultado

Ventana cambiando aleatoriamente de colores

Conclusión

Como puedes ver, ahora puedes utilizar el contexto en todo el componente. Vale la pena recalcar que si necesitas utilizar la referencia al HTMLElement del contexto, puedes utilizar globalmente la propiedad canvasRef.current, o la propiedad de solo lectura canvasContext.canvas. Espero que encuentres esta enfoque fácil y leíble, además que eficiente. Hasta la próxima!

Blog

Mis últimas publicaciones

¿Cómo puedo ayudarte?

Aprendiendo y mejorando cada vez más

Página programada por mí