En mi rol de 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 de sombra 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 valor 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, voy a explicar cómo integrar una aplicación React con componentes web para crear una experiencia de desarrollador (casi) perfecta. Examinaremos las mejores prácticas de React y sus limitaciones, luego crearemos envoltorios genéricos y pragmas JSX personalizados para acoplar más estrechamente nuestros elementos personalizados y el marco más popular de la actualidad.
Colorear en las lineas
Si React es un libro para colorear, perdone la metáfora, tengo dos niños pequeños a los que les encanta colorear, definitivamente hay formas de mantenerse dentro de las líneas para trabajar con elementos personalizados. Para empezar, escribiremos un elemento personalizado muy simple que adjunta una entrada de texto al DOM de sombra y emite un evento cuando cambia el valor. En aras de la simplicidad, usaremos LitElement como base, pero ciertamente puede escribir su propio elemento personalizado desde cero si lo desea.
Nuestra super-cool-input
El elemento es básicamente una envoltura con algunos estilos para un simple ol ‘ <input>
elemento 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 ref
Según la documentación de React para componentes web, “[t]Para acceder a las API imperativas de un componente web, deberá utilizar una referencia para interactuar directamente con el nodo DOM “.
Esto es necesario porque React actualmente no tiene una forma de escuchar eventos DOM nativos (prefiriendo, en cambio, usar su propia propiedad SyntheticEvent
system), ni tiene una forma de acceder declarativamente al elemento DOM actual sin usar una ref.
Haremos uso de React’s useRef
hook para crear una referencia al elemento DOM nativo que hemos definido. También usaremos React’s useEffect
y useState
hooks 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
‘s reportValue
método si el valor es alguna vez una variante de la palabra “rad”.
Una cosa a tener en cuenta en el ejemplo anterior es nuestro componente React useEffect
cuadra.
useEffect(() => {
coolInput.current.addEventListener('custom-input', eventListener);
return () => {
coolInput.current.removeEventListener('custom-input', eventListener);
}
});
El useEffect
block 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 que no tengamos pérdidas de memoria involuntarias.
Si bien el ejemplo anterior simplemente vincula un detector de eventos, esta también es una técnica que se puede emplear para vincular las propiedades DOM (definidas como entradas en el objeto DOM, en lugar de accesorios React o atributos DOM).
Esto no es tan malo. Tenemos nuestro elemento personalizado funcionando en React, y podemos vincularnos a nuestro evento personalizado, acceder al valor de él 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.
Método 2: use una envoltura
Nuestro próximo intento de usar nuestro elemento personalizado en nuestra aplicación React es crear un contenedor para el elemento. Nuestro contenedor es simplemente un componente de React que transmite 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
El componente React gestiona la creación de una referencia mientras agrega y elimina oyentes de eventos para que cualquier componente consumidor pueda pasar accesorios como cualquier otro componente 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 a lo largo del valor actual de la CoolInput
La ref interna.
Usando 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 asume la definición del componente React y la gestión de todo el ciclo de vida.
Enfoque 3: utilice 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 prop adicional a los elementos de React, y cualquier prop que tenga el prefijo onEvent
agrega un detector de eventos al anfitrión.
Para invocar un pragma, necesitamos 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 hacerlo global). Es posible que haya visto esto en bibliotecas como Emotion.
Un <input>
elemento con el onEventInput={callback}
prop ejecutará el callback
funcionar siempre que un evento con el nombre 'input'
se envía. Veamos cómo se ve eso para nuestro super-cool-input
.
El código del pragma está disponible en GitHub. Si desea vincularse a propiedades nativas en lugar de a los accesorios de React, puede usar react-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 los transforma en un nombre de evento, tomando el valor pasado a ese prop (aparentemente una función con la firma (e: Event) => void
) y agregarlo como detector de eventos en la instancia del elemento.
Viendo hacia adelante
En el 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 planes parecen haberse retrasado a la versión 18.
Hasta entonces, será necesario un poco más de trabajo para utilizar todas las funciones que ofrecen los elementos personalizados con React. Con suerte, el equipo de React continuará mejorando el soporte para cerrar la brecha entre React y la plataforma web.