4 pasos para crear una buena API de componente reusable
Creando componentes reusables siempre empiezo dudando de qué API debería tener - en un componente hablamos de entradas y salidas, props que entran y eventos que salen - o en el caso de React, métodos que ejecutan.
TL;DR
- Evita estados imposibles
- Deja espacio para la personalización.
- Piensa en la Developer Experience (DX).
- Evita abstracciones innecesarias
En este caso, el “Too long, didn’t read” nos sirve de índice, ya que vamos a desarrollar cada punto por separado.
1. Evita estados imposibles
Este apartado está fuertemente inspirado en la charla “Making Impossible States Impossible” de Richard Feldman. Básicamente consiste en declarar un modelo de datos que no permite que estados no compatibles colisionen. Si utilizas un lenguaje tipado, te beneficiarás mucho más de esto, pero también podemos hacer esto con JavaScript.
En este otro artículo, Kent C. Dodds lo pone de manifiesto en un ejemplo tal y como lo buscamos, un estado que hemos creado en un componente de React, y que da lugar a posibles estados que no deberían coexistir.
Si tenemos una notificación que puede ser de tipo “info”, “warning” y “error”, el componente puede ser:
const Notification = ({info, warning, error, message}) => {
if (info) {
...
}
return (
...
};
así, podríamos usar el componente de diferentes formas:
<Notification error message="error!" />
<Notification info message="info!" />
Sin embargo, estamos dejando al usuario de nuestro componente la posibilidad de utilizarlo mal, colisionando ciertas props sin saber qué va a pasar. ¿Qué pasará cuando el usuario lo utilice así?
<Notification error info message="mensaje" />
Esto que acabamos de ver es algo a evitar cuando estamos creando un componente reutilizable, y más si lo van a utilizar personas de fuera de nuestro equipo, donde debemos dejar un componente completamente testeado y documentado. Tenemos la opción de crear un test para indicar cuál es el comportamiento correcto en este caso:
describe('when error and info are set', () => {
it('should...')
};
O mejor, evitar ese estado, haciendo que “error”, “info” y “warning” sean valores de un type
:
<Notification type="warning" message="warning message!" />
Como dice Richard Fieldman en la charla que mencionábamos
Testear está bien, pero lo imposible está mejor
2. Deja espacio para la personalización
Personalizar los componentes que usamos de alguna librería es algo muy común, ya sea simplemente personalizar los estilos de un componente, o personalizar alguna parte de la interfaz, sustituyendo por ejemplo un texto por un componente propio.
Este punto es muy abierto y puede sonar poco definido, ya que dependerá del framework que estemos utilizando, bien podemos usar <slot>
si estamos usando WebComponents, o en el caso de React, hacer uso del children
o directamente dejar una prop abierta para el componente a renderizar…
Lo que debemos saber de este punto es que puede que el usuario de nuestro componente lo utilice tal cual, o por su caso de uso necesite un punto extra de flexibilidad.
Un ejemplo muy básico sería un componente que renderiza un listado de items. El componente puede renderizar el item como si fuera cualquier cosa, así que podríamos pasar un listado de items que en realidad sería un listado de JSX que renderizará, incluso cada uno puede tener sus onClick
correspondientes, haciendo que un componente del estilo:
<RenderList data={messages}>
<Message />
<HiddenMessages />
</RenderList>
acepte en su variable data tanto:
const messages = new Array(20).fill().map((_, i) => ({
content: `Hello, this is message ${i} here. Cool, huh?`,
}));
como:
const messages = new Array(20).fill().map((_, i) => ({
content: (
<button key={i} onClick={console.log}>
button {i}
</button>
),
}));
Evita abstracciones innecesarias
Somos desarrolladores. Buscamos siempre cómo aportar lo máximo con abstracciones para permitir todos los casos de uso posibles. A veces esto nos puede llevar a pensar cosas del estilo: bueno, si hago X, esta feature es gratis, así que por qué no).
Sin embargo esta abstracción que en teoría hacemos para ahorrar tiempo al usuario y darle lo máximo, a veces puede ser innecesaria e incluso perjudicial, haciendo que sea demasiado compleja para un usuario que está iniciándose con nuestro componente, y en lugar de ayudarle, si no consigue lo que ha venido a hacer, incluso desista y no lo use.
Siguiendo el patrón que se comenta en esa charla, y tomando como el ejemplo del componente anterior que renderiza una lista, podemos seguir el siguiente proceso mental que nos lleva el desarrollo del mismo:
-
Desarrollamos componente que permite renderizar una lista de items.
-
Creamos la abstracción para que se pueda pasar contenido JSX.
-
Hay un nuevo requerimiento, cada item irá con un icono, y la mayoría de las veces será el mismo.
-
Creamos una abstracción para poder pasar un icono que se repetirá con cada item. Ahora nuestro componente se usaría:
<RenderList data={messages} icon="fa-file"> <Message /> <HiddenMessages /> </RenderList>
-
Esta abstracción nos hace pensar en el tiempo que ahorramos con esa nueva prop.
-
Ahora pensamos qué pasa si queremos diferentes iconos y refactorizamos para poder pasar
icons
:<RenderList data={messages} icons={["fa-file", "fa-file", "fa-danger"]}> <Message /> <HiddenMessages /> </RenderList>
Esto nos ha llevado a una abstracción que parece no ser la mejor a la hora de resolver el problema inicial, ya que el requisito de renderizar también un icono se podía hacer desde el principio, pasándole al componente una lista de JSX personalizada.
Lo que fue una abstracción universal, ahora se comporta diferente para diferentes casos de uso. - Sandi Metz
Piensa en la Developer eXperience
Crea los componentes para el usuario más importante del mundo - tu “yo del futuro”.
Porque todo lo visto anteriormente no tiene por qué ser para un usuario desconocido - puede ser para tu equipo y/o para ti. Y además de pensar en la API, ten en cuenta que una parte del mantenimiento del software es la documentación y el testing, y con el tiempo nos olvidaremos de cosas si no las dejamos por escrito. Redacta una documentación que no dé lugar a dudas y tests que puedan ser usados como documentación, ya que por la sintaxis podemos hacer que se escriban de forma que describan su comportamiento (además es como se debería testear).
La idea es tardar lo mínimo en entender cómo funciona el componente.
Porque la documentación y los tests pueden hacer que “triunfe” nuestro componente, o pueden “romperlo”. Cuando un desarrollador está usando tu componente por primera vez, lo primero que hará será leer la documentación. Si tu guía inicial no está clara, puede que pierdas a este usuario.
Tendemos a enfocarnos mucho en los detalles de implementación y perdemos la visión de los desarrolladores que entran por primera vez y necesitan tener una visión clara de lo que van a usar.
En definitiva, también es nuestro trabajo asegurar que se han tenido en cuenta las necesidades del desarrollador.
Piensa en las veces que como desarrollador, cuando buscas un componente has seguido este proceso:
- Necesitas X
- Buscas y encuentras una librería que en teoría parece que hace X.
- No consigues el resultado que buscas, y desistes, desinstalando la librería y buscando otra o fabricándola tú mismo.
Conclusiones
Espero que este artículo te ayude a la hora de crear tu siguiente componente dentro de un catálogo o a la hora de crear un componente de forma aislada o incluso para crear un catálogo de componentes completo. Estos criterios son los que tengo como fundamento, aunque hay casos especiales y tampoco hay que tener un plan para seguirlo 100% a la perfección, pero con estos 4 puntos seguro que ves tu componente de otra manera 😊. Buena suerte!
expect(newComponent).toBeBetterThan(oldComponent);