3 enfoques para integrar React con elementos personalizados
En mi rol como desarrollador web que se encuentra en la intersección del diseño y el código, me atraen los componentes web debido a su portabilidad. Tiene sentido: los elementos personalizados son elementos HTML completamente funcionales que funcionan en todos los navegadores modernos, y el DOM oculto encapsula los estilos correctos con una superficie decente para la personalización. Es realmente una buena opción, especialmente para organizaciones más grandes que buscan crear experiencias de usuario consistentes en múltiples marcos, como Angular, Svelte y Vue.
En mi experiencia, sin embargo, hay un caso atípico en el que muchos desarrolladores creen que los elementos personalizados no funcionan, específicamente aquellos que trabajan con React, que es, posiblemente, la biblioteca front-end más popular que existe en este momento. Y es cierto, React tiene algunas oportunidades definidas para una mayor compatibilidad con las especificaciones de los componentes web; Sin embargo, la idea de que React no puede integrarse profundamente con los componentes web es un mito.
En este artículo, explicaré cómo integrar una aplicación React con componentes web para crear una experiencia de desarrollador (casi) perfecta. Analizaremos las mejores prácticas y limitaciones de React, luego crearemos contenedores genéricos y pragmas JSX personalizados para acoplar más estrechamente nuestros elementos personalizados y el marco más popular de la actualidad.
Coloreando las lineas
Si React es un libro para colorear (perdón por la metáfora, tengo dos hijos pequeños a quienes les encanta colorear), definitivamente hay maneras de mantenerse dentro de las líneas para trabajar con elementos personalizados. Para comenzar, escribiremos un elemento personalizado muy simple que adjunta una entrada de texto al DOM oculta y emite un evento cuando cambia el valor. En aras de la simplicidad, usaremos LitElement como base, pero ciertamente puedes escribir tu propio elemento personalizado desde cero si lo deseas.
Nuestro super-cool-input
elemento es básicamente un contenedor con algunos estilos para un elemento simple input
que emite un evento personalizado. Tiene un reportValue
método para que los usuarios conozcan el valor actual de la forma más desagradable posible. Si bien este elemento puede no ser el más útil, las técnicas que ilustraremos mientras lo conectamos a React serán útiles para trabajar con otros elementos personalizados.
Método 1: utilizar referencia
Según la documentación de React para componentes web, “[para] acceder a las API imperativas de un componente web, necesitará utilizar una referencia para interactuar directamente con el nodo DOM”.
Esto es necesario porque React actualmente no tiene una manera de escuchar eventos DOM nativos (prefiriendo, en cambio, usar su propio sistema propietario SyntheticEvent
), ni tiene una manera de acceder declarativamente al elemento DOM actual sin usar una referencia.
Usaremos el useRef
gancho de React para crear una referencia al elemento DOM nativo que hemos definido. También usaremos React useEffect
y useState
ganchos para obtener acceso al valor de la entrada y representarlo en nuestra aplicación. También usaremos la referencia para llamar a nuestro super-cool-input
método reportValue
si el valor es alguna vez una variante de la palabra “rad”.
Una cosa a tener en cuenta en el ejemplo anterior es useEffect
el bloque de nuestro componente React.
useEffect(() = { coolInput.current.addEventListener('custom-input', eventListener); return () = { coolInput.current.removeEventListener('custom-input', eventListener); }});
El useEffect
bloque crea un efecto secundario (agregar un detector de eventos no administrado por React), por lo que debemos tener cuidado de eliminar el detector de eventos cuando el componente necesita un cambio para no tener pérdidas de memoria involuntaria.
Si bien el ejemplo anterior simplemente vincula un detector de eventos, esta también es una técnica que se puede emplear para vincular propiedades DOM (definidas como entradas en el objeto DOM, en lugar de accesorios de React o atributos DOM).
Esto no es tan malo. Tenemos nuestro elemento personalizado funcionando en React y podemos vincularnos a nuestro evento personalizado, acceder a su valor y también llamar a los métodos de nuestro elemento personalizado. Si bien esto funciona, es detallado y realmente no se parece a React.
Enfoque 2: use un envoltorio
Nuestro próximo intento de utilizar nuestro elemento personalizado en nuestra aplicación React es crear un contenedor para el elemento. Nuestro contenedor es simplemente un componente de React que pasa accesorios a nuestro elemento y crea una API para interactuar con las partes de nuestro elemento que normalmente no están disponibles en React.
Aquí, hemos trasladado la complejidad a un componente contenedor para nuestro elemento personalizado. El nuevo CoolInput
componente React gestiona la creación de una referencia mientras agrega y elimina detectores de eventos para que cualquier componente consumidor pueda pasar accesorios como cualquier otro componente de React.
function CoolInput(props) { const ref = useRef(); const { children, onCustomInput, ...rest } = props; function invokeCallback(event) { if (onCustomInput) { onCustomInput(event, ref.current); } } useEffect(() = { const { current } = ref; current.addEventListener('custom-input', invokeCallback); return () = { current.removeEventListener('custom-input', invokeCallback); } }); return super-cool-input ref={ref} {...rest}{children}/super-cool-input;}
En este componente, hemos creado un accesorio onCustomInput
que, cuando está presente, activa una devolución de llamada de evento desde el componente principal. A diferencia de una devolución de llamada de evento normal, elegimos agregar un segundo argumento que pasa el valor actual de la CoolInput
referencia interna.
Utilizando estas mismas técnicas, es posible crear un contenedor genérico para un elemento personalizado, como este reactifyLitElement
componente de Mathieu Puech. Este componente en particular se encarga de definir el componente React y gestionar todo el ciclo de vida.
Método 3: utilizar un pragma JSX
Otra opción es usar un pragma JSX, que es como secuestrar el analizador JSX de React y agregar nuestras propias características al lenguaje. En el siguiente ejemplo, importamos el paquete jsx-native-events de Skypack. Este pragma agrega un tipo de accesorio adicional a los elementos de React, y cualquier accesorio que tenga el prefijo onEvent
agrega un detector de eventos al host.
Para invocar un pragma, debemos importarlo al archivo que estamos usando y llamarlo usando el /** @jsx PRAGMA_NAME */
comentario en la parte superior del archivo. Su compilador JSX generalmente sabrá qué hacer con este comentario (y Babel se puede configurar para que sea global). Es posible que hayas visto esto en bibliotecas como Emotion.
Un input
elemento con la onEventInput={callback}
propiedad ejecutará la función cada vez que se envíe callback
un evento con el nombre. 'input'
Veamos cómo se ve eso para nuestro super-cool-input
.
El código de pragma está disponible en GitHub. Si desea vincular propiedades nativas en lugar de accesorios de React, puede usar reaccionar-bind-properties. Echemos un vistazo rápido a eso:
import React from 'react'/** * Convert a string from camelCase to kebab-case * @param {string} string - The base string (ostensibly camelCase) * @return {string} - A kebab-case string */const toKebabCase = string = string.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()/** @type {Symbol} - Used to save reference to active listeners */const listeners = Symbol('jsx-native-events/event-listeners')const eventPattern = /^onEvent/export default function jsx (type, props, ...children) { // Make a copy of the props object const newProps = { ...props } if (typeof type === 'string') { newProps.ref = (element) = { // Merge existing ref prop if (props props.ref) { if (typeof props.ref === 'function') { props.ref(element) } else if (typeof props.ref === 'object') { props.ref.current = element } } if (element) { if (props) { const keys = Object.keys(props) /** Get all keys that have the `onEvent` prefix */ keys .filter(key = key.match(eventPattern)) .map(key = ({ key, eventName: toKebabCase( key.replace('onEvent', '') ).replace('-', '') }) ) .map(({ eventName, key }) = { /** Add the listeners Map if not present */ if (!element[listeners]) { element[listeners] = new Map() } /** If the listener hasn't be attached, attach it */ if (!element[listeners].has(eventName)) { element.addEventListener(eventName, props[key]) /** Save a reference to avoid listening to the same value twice */ element[listeners].set(eventName, props[key]) } }) } } } } return React.createElement.apply(null, [type, newProps, ...children])}
Básicamente, este código convierte cualquier accesorio existente con el onEvent
prefijo y lo transforma en un nombre de evento, tomando el valor pasado a ese accesorio (aparentemente una función con la firma (e: Event) = void
) y agregándolo como un detector de eventos en la instancia del elemento.
Pensando en el futuro
Al momento de escribir este artículo, React lanzó recientemente la versión 17. El equipo de React había planeado inicialmente lanzar mejoras para la compatibilidad con elementos personalizados; desafortunadamente, esos aviones parecen haber sido pospuestos a la versión 18.
Hasta entonces, será necesario un poco de trabajo adicional para utilizar todas las funciones que ofrecen los elementos personalizados con React. Con suerte, el equipo de React comenzará mejorando el soporte para cerrar la brecha entre React y la plataforma web.
Deja un comentario