Integrar cualquier librería JavaScript con React

¿Trabajas con React y necesitas integrar una librería JavaScript de terceros? estás en el lugar adecuado.

En este artículo aprenderás a implementar las técnicas necesarias para crear componentes personalizados que incorporen funcionalidades de otras librerías JavaScript. La mejor forma de aprender es practicando. De modo que te guiaré para que, al final del post, seas capaz de crear un componente React basado en la librería PixiJS como el siguiente.

Haz click sobre la imagen para ver el resultado final de este tutorial

¿Quieres saltarte la introducción? Haz click aquí para ir directamente a la guía de integración

Día tras día, una gran comunidad, crea y mantiene librerías y recursos de gran calidad. Y gracias a esto, cualquier desarrollador, puede implementar funcionalidades de todo tipo en sus proyectos. Ahorrando con ello miles de horas en su trabajo.

La mayoría de herramientas que analizo por aquí, funcionan de forma independiente. Sin la necesidad de enmarcarse obligatoriamente en ningún otro recurso. Esa versatilidad me encanta, y por eso, generalmente estudio recursos libres de dependencias. 

Sin embargo, no se debe ignorar que ciertos «frameworks» y librerías han adquirido una gran popularidad en los últimos años. Hasta el punto de convertirse en una base esencial para el desarrollo de muchos proyectos. El secreto de su fama se debe al enorme potencial de desarrollo que ofrecen.

React, Angular, Vue, Svelte o SolidJs son algunos de los ejemplos más relevantes. 

Evidentemente, cada una de estas tecnologías tiene su propia curva de aprendizaje, pero una vez superada, se convierten en herramientas realmente potentes.

Sin embargo, al utilizarlas pueden aparecer otras dificultades. Debido a que cada una de estas librerías determina su propio flujo de trabajo, no siempre es fácil saber cómo integrar en ellas recursos propios y de terceros.

Por supuesto, la comunidad trabaja incansablemente para ofrecer “wrappers” que faciliten la compatibilidad entre estos “frameworks” y otras soluciones.

Un buen ejemplo de esto es React Three Fiber. Éste ofrece la posibilidad de utilizar la fantástica librería ThreeJs mediante declarativos componentes de React.

De hecho, se llegan a desarrollar bibliotecas enteras de componentes con ese propósito. Como MaterialUI para React, Vuetify para Vue, o Ionic, que da soporte a múltiples frameworks.

Aún así, es posible encontrarse con determinados recursos, que no disponen de un “wrapper” que conecte su funcionalidad con nuestro marco de trabajo favorito.

Por eso, hoy voy a explicar cómo integrar cualquier librería JavaScript con uno de los “frameworks” más populares del momento, React.

Guantalete del infinito para dominar todas las tecnologias con React
Guantalete del infinito para dominar todas las tecnologias con React

React es mi opción número uno, en lo que a marcos de trabajo frontend se refiere (aunque técnicamente no sea un framework). Conocer bien cómo utilizar React, puede llevar un tiempo, y escapa al ámbito de este artículo. Así que para poder seguir el siguiente tutorial, es necesario tener experiencia con su API.

Si te interesa aprender React desde cero, al final de este artículo te dejaré varios recursos online geniales. Y por supuesto, tarde o temprano, le daré un vistazo más detallado tanto a ésta, como al resto de tecnologías mencionadas.

Crear un componente React a partir de cualquier librería JavaScript

Pero basta de introducción. Vamos a crear un componente React personalizado a partir de una librería JavaScript de terceros.

En esta ocasión utilizaremos la famosa librería de renderizado de gráficos PixiJs.

https://pixijs.com/

Crear un componente React a partir de ésta herramienta resulta especialmente interesante. En seguida verás porqué. 

Internamente, PixiJs utiliza la API WebGL. Una tecnología integrada en el navegador, que aprovecha el potencial de la GPU para calcular y pintar gráficos en pantalla de forma eficiente.

Sin embargo, el resultado se visualiza dentro de una etiqueta “canvas”. Por consiguiente, toda la interacción que el usuario realice en este contenedor, no se podrá registrar a través del DOM.

Entonces, siendo React una librería UI, que precisamente se encarga de manipular el DOM ¿Cómo se puede diseñar un componente, que integre las capacidades de PixiJs en el flujo de trabajo de React?

No te preocupes, lo iremos viendo paso a paso. 

Este tutorial está estructurado en 4 secciones, (si en algún momento lo consideras oportuno, siéntete libre de saltar a la siguiente):

  • Preparar el entorno de desarrollo con React.
  • Crear un servicio a partir de la librería PixiJs.
  • Definir el componente vinculado al servicio.
  • Instanciar y configurar el componente.

Antes de entrar en materia con el primer punto, te animo a que tengas a mano el repositorio del ejercicio. Ya que te puede ser de ayuda para seguir los siguientes párrafos.

https://github.com/Danivalldo/libreriasjs/tree/master/library-to-component

Preparar el entorno de desarrollo con React.

Para empezar, es necesario preparar un entorno de desarrollo. 

Si tienes un poco de experiencia trabajando con React, ya sabrás que la forma más rápida y cómoda de hacerlo, es mediante la instrucción «npx create-react-app«, seguida del nombre del proyecto.

npx create-react-app library-to-component

A través de éste comando generamos un espacio de trabajo nuevo, con todo lo necesario para ejecutar y compilar nuestra webapp.

Cuando termine la instalación, eliminaremos aquello que no sea esencial para nuestro proyecto. Dejando el conjunto de archivos como en la imagen de a continuación.

Directorios del proyecto library-to-component
Directorio del proyecto library-to-component

Como ves, también añadimos dos directorios nuevos que de momento estarán vacíos. A uno lo vamos a llamar “services” y al otro “components”.

También editaremos el archivo App.js para que quede de la siguiente forma

function App() {
  return <div className="App">Hello world</div>;
}
export default App;

Adicionalmente, sugiero comentar el Modo “strict” en el archivo index.js. Este modo solo tiene efecto durante la fase de desarrollo. Desactivarlo es opcional, pero tienes que tener en cuenta que puede provocar comportamientos imprevisibles. Especialmente en el tipo de integración que vamos a hacer. 

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
  // <React.StrictMode>
  <App />
  // </React.StrictMode>
);

Por supuesto, también tenemos que instalar PixiJs como dependencia. Para hacerlo, lanza el siguiente comando en tu CLI.

npm install pixi.js

Con estos ajustes realizados, podemos iniciar el servidor local con el siguiente comando

npm start

En este punto, tu aplicación solo debería mostrar el texto «Hello world».

Crear un servicio a partir de la librería PixiJs.

Para simplificar un poco las cosas, partiremos de un ejemplo que se encuentra en la página oficial de PixiJs. Se trata de un sencillo script que permite revelar una imagen oculta a base de «rascar» o “pintar” sobre otra que se encuentra superpuesta.

https://pixijs.io/examples/#/demo

Aunque no conozcas la API de PixiJs al detalle, ésta demo en particular es bastante fácil de entender. Te animo a que leas su código fuente y trates de comprenderlo, ya que las siguientes líneas usarán gran parte de su código.

¿Lo tienes? Bien. El primer paso consistirá en crear una clase que contenga su lógica. Hacerlo así, nos va a permitir instanciar varios objetos que encapsulan el comportamiento de forma independiente.

Así pues, creamos un archivo con el nombre ScratchCardService.js, y lo guardamos en el directorio /services.

En este documento declaramos la clase con el mismo nombre del archivo. De momento la dejamos vacía, pero la iremos escalando progresivamente en los siguientes puntos.

También aprovechamos para importar algunos recursos de la librería PixiJs justo al inicio.

import {
  Application,
  Graphics,
  RenderTexture,
  Sprite,
  filters,
  Texture,
} from "pixi.js";
class ScratchCardService {
  
}
export default ScratchCardService;

Establecer todos los métodos y propiedades de esta clase va a ser, sin duda, la parte más larga de esta guía. Pero una vez completado, el resto de puntos se resuelven casi inmediatamente.

Comenzamos definiendo el constructor y sus propiedades con valores “undefined” por defecto. Este es el listado de todas ellas, acompañadas de una breve descripción del valor que contendrán.

  • app: Una instancia de la aplicación PixiJs.
  • stage: La escena contenedora de los elementos que PixiJs renderiza.
  • container: El contenedor del DOM donde se enmarcará la escena.
  • brush: Un gráfico generado con PixiJs en forma de circunferencia. Lo utilizaremos a modo de punta de pincel para revelar la imagen oculta.
  • dragging: Una variable booleana para controlar si el usuario está arrastrando el “pincel” o no.
  • renderTexture: Esta variable guardará una imagen en blanco y negro del trazo generado por el usuario con el pincel. 
  • background: El Sprite de la imagen a revelar.
  • imageToReveal: El Sprite de la imagen superpuesta.
  • renderTextureSprite: Este Sprite tendrá como textura “renderTexture”. Y nos servirá para aplicarla como máscara de “imageToReveal”.
  • listeners: Se trata de un objeto que guardará posibles funciones de callback creadas por el desarrollador que use la clase. Estas funciones se ejecutarán de forma opcional cuando el usuario interactúe con el servicio. Los posibles eventos que establecemos son “scratchstart”, “scratchend” o “scratching”.

Complementariamente a las propiedades, también guardaremos referencias a otros métodos de la clase, que declaramos justo después del constructor.

constructor() {
  this.app = undefined;
  this.stage = undefined;
  this.container = undefined;
  this.brush = undefined;
  this.dragging = false;
  this.renderTexture = undefined;
  this.background = undefined;
  this.imageToReveal = undefined;
  this.renderTextureSprite = undefined;
  this.listeners = {
     scratchstart: undefined,
     scratchend: undefined,
     scratching: undefined,
  };
  this.handleOnResize = this.onResize.bind(this);
  this.handleOnPointerDown = this.pointerDown.bind(this);
  this.handleOnPointerUp = this.pointerUp.bind(this);
  this.handleOnPointerMove = this.pointerMove.bind(this);
}  
pointerMove(event) {
}
pointerDown(event) {
}
pointerUp(event) {
}
onResize() {
}

Guardar referencias a otros métodos nos va a resultar útil a la hora de crear y destruir «listeners»

El primer método de la clase que vamos a definir es “launch”, y lo preparamos para que reciba dos parámetros. 

El primer argumento será el elemento del DOM donde se desea que PixiJS cargue la escena.

El segundo se trata de un objeto JavaScript que agrupa tres propiedades de configuración. La URL de la imagen frontal, la URL de la imagen oculta y el radio del pincel.

launch(container, { frontImage, backImage, radius = 50 }) {
}

A continuación te iré mostrando las instrucciones que lo conforman, y describiré la lógica que hay detrás de ellas. Tal y como ya dije, te recomiendo que tengas el código del repositorio cerca, por si te pierdes en algún punto.

Al ejecutar el método, guardamos en la propiedad “container” la referencia al contenedor del DOM pasado por parámetro. Obtenemos la dimensión de dicho contenedor con el método “getBoundingClientRect()”. 

Aprovechamos el valor devuelto, para crear una instancia de aplicación de PixiJS con el mismo tamaño, y la asignamos a la propiedad app.

this.container = container;
const containerSize = this.container.getBoundingClientRect();
this.app = new Application({
  width: containerSize.width,
  height: containerSize.height,
  resizeTo: container,
});

Seguidamente guardamos una referencia a la propiedad “stage” de “app”. Y establecemos “view”, como hijo de nuestro contenedor mediante el método “appendChild”.

Aunque todavía no está declarado, preparamos una llamada al método setBrush pasando “raduis” como único argumento. 

this.stage = this.app.stage;
this.container.appendChild(this.app.view);
this.setBrush(radius);

En las instrucciones que siguen, creamos Sprites a partir de las imágenes entregadas como parámetros, y las añadimos a la escena a través de “addChild”. Por supuesto “background” será la imagen de fondo, y “imageToReveal” corresponderá a la imagen que el usuario irá descubriendo.

this.background = new Sprite(Texture.from(backImage));
this.stage.addChild(this.background);
this.imageToReveal = new Sprite(Texture.from(frontImage));
this.stage.addChild(this.imageToReveal);

El propósito de las siguientes líneas, es preparar una textura sobre la cual generar el efecto de rascar (o pintar) para revelar la imagen. El usuario “pintará” en una imagen vacía, y ésta actuará de máscara de “imageToReveal”

this.renderTexture = RenderTexture.create({
  width: this.app.screen.width,
  height: this.app.screen.height,
});
this.renderTextureSprite = new Sprite(this.renderTexture);
this.stage.addChild(this.renderTextureSprite);
this.imageToReveal.mask = this.renderTextureSprite;

Cerramos el método launch llamando a la función “onResize” que declararemos más tarde. Marcamos como interactiva la escena de PixiJS. Y definimos los listeners “pointerdown”, “pointerup”, “pointermove” i “resize”.

this.onResize();
this.stage.interactive = true;
this.stage.on("pointerdown", this.handleOnPointerDown);
this.stage.on("pointerup", this.handleOnPointerUp);
this.stage.on("pointermove", this.handleOnPointerMove);
window.addEventListener("resize", this.handleOnResize);

Con esto damos por cerrado el método “launch”. Sin duda, el más complejo de toda la clase.

Pero vamos a seguir ampliando el servicio, creando el método setBrush. Como verás, éste sencillamente genera una circunferencia a partir de la clase Graphics de PixiJs, con opción a establecer su radio y desenfoque.

setBrush(radius) {
  this.brush = new Graphics();
  this.brush.beginFill(0xffffff);
  this.brush.drawCircle(0, 0, radius);
  this.brush.filters = [new filters.BlurFilter(5)];
  this.brush.endFill();
}

Las siguientes funciones que vamos a generar son las relativas a los eventos que hemos capturado al final de “launch”. Comenzaremos por onResize, encargada de actualizar el tamaño de la escena y los elementos que la integran.

onResize() {
  this.app.resize();
  this.background.width = this.app.screen.width;
  this.background.height = this.app.screen.height;
  this.imageToReveal.width = this.app.screen.width;
  this.imageToReveal.height = this.app.screen.height;
  this.renderTexture.resize(
    this.app.screen.width,
    this.app.screen.height,
    true
  );
  this.renderTextureSprite.width = this.app.screen.width;
  this.renderTextureSprite.height = this.app.screen.height;
}

Seguiremos con “pointerDown”, la función de retorno que se ejecuta cuando el usuario inicia un click (o un tap) sobre la escena.

PointerDown establece como true el estado de dragging. Luego comprueba si se ha asociado alguna función al evento personalizado “scratchstart”, y si así es, la ejecuta. Para terminar, llama a pointerMove pasándole el mismo evento capturado.

pointerDown(event) {
  this.dragging = true;
  if (this.listeners["scratchstart"]) {
    this.listeners["scratchstart"](event);
  }
  this.pointerMove(event);
}

Por consiguiente, es necesario definir “pointerMove”. Al inicio del método se comprueba si existe “event” y si “dragging” es true para poder continuar su función. En caso de que pase el filtro, se usa la información del evento, para actualizar la posición del pincel

Tras hacerlo, se actualiza la imagen “renderTexture” con las opciones de “clear” y “skipUpdateTransform” como false.

Una vez más comprobamos si existe alguna función del usuario asignada al evento “scratching”, y la ejecutamos.

pointerMove(event) {
  if (!event || !this.dragging) {
    return;
  }
  this.brush.position.copyFrom(event.data.global);
  this.app.renderer.render(this.brush, {
    renderTexture: this.renderTexture,
    clear: false,
    transform: null,
    skipUpdateTransform: false,
  });
  if (this.listeners["scratching"]) {
    this.listeners["scratching"](event);
  }
}

PointerUp, establece el estado de “dragging” como false, y también lanza una función opcional asociada al evento “scratchend”.

pointerUp(event) {
  this.dragging = false;
  if (this.listeners["scratchend"]) {
    this.listeners["scratchend"](event);
  }
}

En estos tres métodos hemos visto que se comprueba y se ejecutan funciones de “callback” opcionales. Pero de momento, no existe ninguna forma para que un desarrollador pueda vincular sus funciones personalizadas. Es hora de corregir eso, creando el método “on”.

“On” recibe un parámetro “eventKey” en forma de cadena de texto, y una función “cb”. Tras hacer la comprobación necesaria, asocia el “eventKey” a esa función dentro de su propiedad listeners.

on(eventKey, cb) {
  if (typeof cb !== "function") {
    return;
  }
  this.listeners[eventKey] = cb;
}

Además de ofrecer la posibilidad al desarrollador de asociar callbacks personalizados, sería interesante que pueda actualizar las imágenes sin necesidad de regenerar la instancia. Por eso vamos construir el método “updateImages”.

Esta función sólo recibe dos parámetros y los usa para actualizar las texturas de los Sprites de imágenes.

updateImages(frontImage, backImage) {
  this.background.texture = Texture.from(frontImage);
  this.imageToReveal.texture = Texture.from(backImage);
}

Finalmente, acabamos con la definición de “destroy”, el método que se lanzará justo antes de destruir la instancia. Este método es imprescindible, ya que si no se gestiona correctamente, podrían aparecer problemas de “Garbage collection”.

Como verás, se encarga de eliminar todos los listeners, lanzar el método destroy, propio de la app de PixiJs. Y restablecer todas las variables al estado inicial.

destroy() {
  window.removeEventListener("resize", this.handleOnResize);
  this.stage.off("pointerdown", this.handleOnPointerDown);
  this.stage.off("pointerup", this.handleOnPointerUp);
  this.stage.off("pointermove", this.handleOnPointerMove);
  this.app.destroy(true, {
    children: true,
    texture: true,
    baseTexture: true,
  });
  this.app = undefined;
  this.stage = undefined;
  this.container = undefined;
  this.brush = undefined;
  this.dragging = false;
  this.renderTexture = undefined;
  this.background = undefined;
  this.imageToReveal = undefined;
  this.renderTextureSprite = undefined;
  this.listeners = {
    scratchstart: undefined,
    scratchend: undefined,
    scratching: undefined,
  };
}

Con esto damos por cerrada la creación del servicio. Ha sido largo, pero era necesario, ahora está todo listo para crear un componente React a partir de esta clase.

Vamos a ver cómo, en el siguiente apartado.

Crear el componente, y vincularlo al servicio.

Ha llegado el momento de crear nuestro componente personalizado, que actúe de “wrapper” del servicio que acabamos de crear.

Empezamos creando un archivo que se llame ScratchCard, y lo guardamos en componentes.

En él, importamos algunas dependencias de “react” y nuestra clase. Seguidamente creamos un componente React con el mismo nombre del archivo, y lo exportamos justo al final.

import { useEffect, useMemo, useRef } from "react";
import ScratchCardService from "../services/ScratchcardService";
const ScratchCard = () => {
  return (
    <></>
  );
};
export default ScratchCard;

Lo primero, es necesario prepararlo para que admita las siguientes propiedades: 

  • frontImage: La URL de la imagen frontal.
  • backImage: La URL de la imagen oculta.
  • radius: El radio del pincel
  • onScratchStart: La función de callback al inicio de la interacción del usuario.
  • onScratchEnd: La función de callback al final de la interacción del usuario.
  • onScratching: La función de callback durante la interacción del usuario.
  • className: El nombre de la clase CSS que se desea asignar al contenedor.
  • style: Estilos en línea que se quieran agregar al contenedor.

En este punto, también aprovechamos para definir el valor a devolver con un contenedor “div” que asigne className y style. Más adelante nos resultará útil tener una referencia a esa “div”, de modo que vamos a crear y asignar con “useRef” la variable “containerRef”.

const ScratchCard = ({
  frontImage,
  backImage,
  radius,
  onScratchStart,
  onScratchEnd,
  onScratching,
  className,
  style,
}) => {
  const containerRef = useRef(null);
  return (
    <div ref={containerRef} className={className} style={{ ...style }}></div>
  );
};

Acto seguido, utilizamos el hook “useMemo”. En su función creamos una instancia de nuestro servicio “ScratchCardService”, y la guardamos con el nombre “scratchCardSrv”. 

Es importante que no se añada nada en el array de dependencias del hook. Ya que nos interesa que el objeto resultante no se genere de nuevo cada vez que el componente se actualiza.

const scratchCardSrv = useMemo(() => {
  return new ScratchCardService();
}, []);

En vez de “useMemo”, habrá quien prefiera usar “useState”. Sin embargo, al no ser una variable que realmente interfiera en el ciclo de renderizado del componente, es mejor no utilizar ese otro hook.

Ya solo nos queda conectar los cambios de propiedades, con llamadas a los métodos del objeto instanciado. Para ello, “useEffect” nos será de enorme utilidad.

Primero vamos a declarar el “side effect” principal, que se ejecuta solo en el momento de montar y desmontar el componente. Por eso, es importante que sus dependencias sean «scratchCardSrv» y «containerRef». Se va a encargar de lanzar el método “launch” sólo una vez, pasando los argumentos necesarios.

También imponemos que se lance el método “destroy” justo antes de que el componente se desmonte.

  useEffect(() => {
    scratchCardSrv.launch(containerRef.current, {
      frontImage,
      backImage,
      radius,
    });
    return () => {
      scratchCardSrv.destroy();
    };
  }, [scratchCardSrv, containerRef]);

A partir de aquí, cada vez que alguna de las propiedades se modifique, capturaremos el cambio con otro “useEffect”. Las dependencias en estos casos serán las propiedades alteradas, y en cada caso, se llamará a un método específico.

Empezamos con los más relevantes. Cuando cambian “frontImage”, “backImage” o “radius”

useEffect(() => {
  scratchCardSrv.updateImages(frontImage, backImage);
}, [frontImage, backImage]);
useEffect(() => {
  scratchCardSrv.setBrush(radius);
}, [radius]);

Eventualmente, también incluimos la actualización de los callbacks opcionales, tal y como aparece aquí.

useEffect(() => {
  if (typeof onScratchStart !== "function") {
    return;
  }
  scratchCardSrv.on("scratchstart", onScratchStart);
}, [onScratchStart]);
useEffect(() => {
  if (typeof onScratchEnd !== "function") {
    return;
  }
  scratchCardSrv.on("scratchend", onScratchEnd);
}, [onScratchEnd]);
useEffect(() => {
  if (typeof onScratching !== "function") {
    return;
  }
  scratchCardSrv.on("scratching", onScratching);
}, [onScratching]);

¡Enhorabuena!, si has llegado hasta aquí, es que has conseguido el objetivo de este artículo. Acabas de crear un componente React personalizado, que aprovecha las funciones de otra librería JavaScript.

Instanciar, y customizar el componente.

Para terminar, solo queda jugar un poco con el componente que acabas de crear. En mi caso, he preparado una interfaz interactiva.

Volviendo al archivo App.js, lo he modificado para que al inicio importe el hook “useState” y el componente “ScratchCard”.

import { useState } from "react";
import ScratchCard from "./components/ScratchCard";
function App() {
  return <div className="App">Hello world</div>;
}
export default App;

A continuación, he creado cuatro estados, y tres funciones de callback. La variable “active” servirá para decidir si se monta un componente o no. “radius” controlará el tamaño del pincel en uno de los componentes. Y frontImage, y backImage, guardarán la url de las imágenes del componente.

Las funciones de callback sencillamente mostrarán en la consola la interacción que está teniendo el usuario en cada momento.

const [active, setActive] = useState(true);
const [radius, setRadius] = useState(20);
const [frontImage, setFrontImage] = useState(
  "https://picsum.photos/id/60/500/500"
);
const [backImage, setBackImage] = useState(
  "https://picsum.photos/id/89/500/500"
);
const handleOnScratchStart = () => {
  console.log("Scratch start");
};
const handleOnScratchEnd = () => {
  console.log("Scratch end");
};
const handleOnScratching = () => {
  console.log("Scratching");
};

El JSX que se va a devolver al final de App, empieza anidando una instancia del componente ScratchCard. Las propiedades de este componente se recogen de los estados declarados al inicio.

return (
  <div className="App">
    {active && (
      <ScratchCard
        frontImage={frontImage}
        backImage={backImage}
        radius={radius}
        style={{ width: 700, height: 700 }}
        className="cont-a"
        onScratchEnd={handleOnScratchEnd}
        onScratchStart={handleOnScratchStart}
        onScratching={handleOnScratching}
      />
    )}
    <div className="ui">
      <div>
        <span className="font-bold">Activo:</span>
        <input
          type="checkbox"
          checked={active}
          onChange={() => {
            setActive((prevActive) => {
              return !prevActive;
            });
          }}
        />
      </div>
      <hr />
      <div>
        <label htmlFor="">Imagen superpuesta</label>
        <input
          type="text"
          value={frontImage}
          onChange={(e) => {
            if (!e.currentTarget.value) {
              return;
            }
            setFrontImage(e.currentTarget.value);
          }}
        />
        <hr />
        <label htmlFor="">Imagen oculta</label>
        <input
          type="text"
          value={backImage}
          onChange={(e) => {
            if (!e.currentTarget.value) {
              return;
            }
            setBackImage(e.currentTarget.value);
          }}
        />
      </div>
      <hr />
      <label htmlFor="">Radio del pincel</label>
      <input
        type="range"
        min={5}
        max={50}
        value={radius}
        onChange={(e) => {
          setRadius(e.currentTarget.value);
        }}
      />
    </div>
    <hr />
    <div>
      <h2 className="text-center">Más ejemplos</h2>
    </div>
    <ScratchCard
      frontImage={"textures/front-image.png"}
      backImage={"textures/back-image.png"}
      radius={25}
      style={{ width: 737, height: 393 }}
      className="cont-a"
    />
    <ScratchCard
      frontImage={"textures/blurred_dog.png"}
      backImage={"textures/dog.png"}
      radius={50}
      style={{ width: 737, height: 823 }}
      className="cont-a"
    />
  </div>
);

Toda la parte que se engloba en el contenedor ui, sirve sencillamente para que el usuario pueda editar dinámicamente los estados. Y de forma reactiva, también el primer componente ScratchCard.

Al final he incluido un par de ejemplos más, para ofrecer distintos resultados.

Antes de dar por terminada esta guía, he añadido algunos estilos CSS en el archivo index.css.

@import url('https://fonts.googleapis.com/css2?family=Heebo:wght@300;700&display=swap');
*{
  box-sizing: border-box;
}
.App{
  padding:10px;
}
body {
  color: #4b4b9e;
  margin: 0;
  background-color: #e7e5ff;
  font-family: 'Heebo', sans-serif;
}
hr{
  opacity: .3;
}
.font-bold{
  font-weight: 700;
}
.text-center{
  text-align: center;
}
.cont-a {
  margin: 0 auto 20px auto;
  box-shadow: 0 4px 5px rgba(0, 0, 0, 0.2);
  overflow: hidden;
  border-radius: 24px;
}
label {
  display: block;
  font-weight: 700;
}
input[type="text"],
input[type="range"]{
  display: block;
  width: 100%;
  padding: 10px;
  font-size: 1.3rem;
  font-family: 'Heebo', sans-serif;
}
.ui{
  padding: 40px;
}
@media screen and (max-width: 850px) {
  .cont-a{
    max-width: 500px;
    max-height: 500px;
  }
  .App :nth-child(5){
    max-width: 500px;
    max-height: 300px;
  }
}
@media screen and (max-width: 530px) {
  .cont-a{
    border-radius: 10px;
    max-width: 300px;
    max-height: 300px;
  }
  .App :nth-child(5){
    max-width: 300px;
    max-height: 200px;
  }
}

Algunas observaciones a tener en cuenta

Ahora sí, si estás leyendo estas líneas significa que has completado del todo el tutorial, ¡de nuevo felicidades!. 

Para ir cerrando, tan solo me gustaría compartir algunas conclusiones y observaciones.

A pesar de que éste artículo se basa en una librería muy específica, mi objetivo era darte una serie de técnicas que se pueden aplicar a cualquier otro recurso JavaScript.

Si consideras que lo he logrado, me doy por satisfecho. Si quieres puedes seguirme a través de las redes sociales, o compartir este link con otras personas que les pueda interesar.

Por otra parte, ¿crees que falla en algo, o el contenido no es del todo preciso?, para mí sería genial que te pusieras en contacto conmigo para compartir cualquier comentario.

Instagram de libreriasjs

Twitter de libreriasjs

Casi se me olvida, si deseas seguir ampliando tu formación, aquí te dejo material adicional completamente gratuito.

Hasta la próxima, un abrazo desarrolladores.

Deja un comentario