Construyendo una biblioteca de componentes con React y Emotion

Una biblioteca de componentes ayuda a mantener un diseño coherente en múltiples proyectos. Garantiza la coherencia porque cualquier cambio realizado se propagará a través de los proyectos que lo utilicen. En este tutorial, aprenderemos cómo construir una biblioteca de componentes usando Emotion en React para resolver inconsistencias.
Según Clearleft, una biblioteca de componentes es:
"Una colección de componentes, organizados de manera significativa y que a menudo (pero no necesariamente) brindan alguna forma de explorar y obtener una vista previa de esos componentes y sus activos asociados".
— “ Sobre la construcción de bibliotecas de componentes ”, Clearleft
Aprenderemos cómo construir una biblioteca de componentes creando una que consta de cuatro componentes:
Button
Un contenedor alrededor del botón HTML predeterminadoBox
Un contenedor (HTML div) con propiedades personalizadasColumns
Un contenedor cuyos hijos están espaciados uniformemente a lo largo del eje x.Stack
Un contenedor cuyos hijos están espaciados uniformemente a lo largo del eje y
Estos componentes podrían luego usarse en cualquier aplicación en la que estemos trabajando. Construiremos la biblioteca de componentes usando React y Emotion.
Al final de este artículo, debería poder crear una biblioteca de componentes que se ajuste a cualquier caso de uso que tenga en mente. Este conocimiento será útil cuando trabaje con un equipo que necesite utilizar componentes reutilizables.
Primero, comencemos por establecer qué es la biblioteca Emotion. La documentación explica:
“Emotion es una biblioteca diseñada para escribir estilos CSS con JavaScript. Proporciona una composición de estilo potente y predecible, además de una excelente experiencia para el desarrollador con funciones como mapas de origen, etiquetas y utilidades de prueba”.
— “ Introducción ”, Documentos de emociones
En esencia, Emotion es una biblioteca de CSS en JavaScript, y una cosa interesante de las bibliotecas de CSS en JavaScript es que le permiten colocar componentes con estilos. Poder unirlos en un alcance garantiza que algunos estilos de componentes no interfieran con otros, lo cual es crucial para nuestra biblioteca de componentes.
Emotion expone dos API para React:
@emotion/core
@emotion/styled
Antes de profundizar en cómo funcionan estas API, tenga en cuenta que ambas admiten el estilo de componentes con cadenas de plantilla y objetos.
La API principal es en realidad como la propiedad normal style
que usamos actualmente cuando creamos aplicaciones con React, con la adición de prefijos de proveedores, selectores anidados, consultas de medios y más.
El uso del enfoque de objetos con la API principal normalmente se vería así:
import { jsx } from '@emotion/core'let Box = props = { return ( div css={{ backgroundColor: 'grey' }} {...props} / )}
Este es un ejemplo bastante artificial que muestra cómo podríamos diseñar un Box
componente con Emotion. Es como cambiar una style
propiedad por otra css
y luego estamos listos.
Ahora, veamos cómo podríamos usar el enfoque de cadena de plantilla con la misma API principal:
import { jsx, css } from '@emotion/core'let Box = props = { return ( div css={css` background-color: grey `} {...props} / )}
Todo lo que hicimos fue envolver la cadena de la plantilla con la css
función de etiqueta y Emotion se encarga del resto.
La API con estilo , que se basa en la API principal, adopta un enfoque ligeramente diferente para diseñar componentes. Esta API se llama con un elemento HTML o componente React particular, y ese elemento se llama con un objeto o una cadena de plantilla que contiene los estilos para ese elemento.
Veamos cómo podríamos usar el enfoque de objetos con la API con estilo:
import styled from '@emotion/styled'const Box = styled.div({ backgroundColor: 'grey'});
A continuación se muestra una forma de utilizar la API con estilo, que es una alternativa al uso de la API principal. Las salidas renderizadas son las mismas.
- Aproveche la sólida recuperación de datos y el tamaño de paquete optimizado con KendoReact Server Data Grid Probar ahora
Ahora, veamos cómo podríamos usar el enfoque de cadena de plantilla usando la API con estilo:
import styled from '@emotion/styled'const Box = styled.div` background-color: grey`
Esto logra lo mismo que el enfoque de objetos, solo que esta vez con una cadena de plantilla.
Podríamos utilizar la API principal o la API con estilo al crear componentes o una aplicación. Prefiero el enfoque con estilo para una biblioteca de componentes por un par de razones:
- Consigue mucho con unas pocas pulsaciones de teclas.
- Se necesita un
as
accesorio, que ayuda a cambiar dinámicamente el elemento HTML desde el sitio de la llamada. Digamos que por defecto usamos un elemento de párrafo y necesitamos un elemento de encabezado debido a la semántica; podemos pasar el elemento de encabezado como un valor a laas
propiedad.
Empezando #
Para comenzar, clonemos los scripts de configuración en GitHub, lo cual podemos hacer en la línea de comando:
git clone git@github.com:smashingmagazine/component-library.git
Este comando copia el código en ese repositorio a la component-library
carpeta. Contiene el código necesario para configurar una biblioteca de componentes, que incluye Rollup para ayudar a agrupar nuestra biblioteca.
Actualmente tenemos una components
carpeta con un index.js
archivo, que no hace nada. Crearemos nuevas carpetas debajo de la components
carpeta para cada componente que construyamos en nuestra biblioteca. La carpeta de cada componente expondrá los siguientes archivos:
Component.js
Este es el componente que estamos construyendo.index.js
Esto exporta el componenteComponent.js
y facilita la referencia a componentes desde una ubicación diferente.Component.story.js
Básicamente, esto representa nuestro componente en sus múltiples estados usando Storybook.
También viene con una utils
carpeta que define ciertas propiedades que se usarían en nuestros componentes. La carpeta contiene varios archivos:
helpers.js
Contiene funciones auxiliares que usaremos en nuestra aplicación.units.js
Esto define las unidades de espaciado y tamaño de fuente, que usaremos más adelante.theme.js
Esto define la paleta, las sombras, la tipografía y la forma de nuestra biblioteca de componentes.
Veamos lo que hemos definido en el units.js
archivo:
export const spacing = { none: 0, xxsmall: '4px', xsmall: '8px', small: '12px', medium: '20px', gutter: '24px', large: '32px', xlarge: '48px', xxlarge: '96px',};export const fontSizes = { xsmall: '0.79rem', small: '0.889rem', medium: '1rem', large: '1.125rem', xlarge: '1.266rem', xxlarge: '1.424rem',};
Esto define las reglas spacing
y fontSizes
. La regla de espaciado se inspiró en el sistema de diseño Braid , que se basa en múltiplos de cuatro. Se fontSizes
derivan de la escala de tipo segundo mayor (1,125), que es una buena escala para sitios web de productos. Si tiene curiosidad por saber más sobre la escala de tipos, “ Exploración de las escalas de tipos responsivos ” explica el valor de conocer las escalas apropiadas para diferentes sitios web.
A continuación, ¡repasemos el theme.js
archivo!
import { spacing } from './units';const white = '#fff';const black = '#111';const palette = { common: { black, white, }, primary: { main: '#0070F3', light: '#146DD6', contrastText: white, }, error: { main: '#A51C30', light: '#A7333F', contrastText: white, }, grey: { 100: '#EAEAEA', 200: '#C9C5C5', 300: '#888', 400: '#666', },};const shadows = { 0: 'none', 1: '0px 5px 10px rgba(0, 0, 0, 0.12)', 2: '0px 8px 30px rgba(0, 0, 0, 0.24)',};const typography = { fontFamily: "Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Ubuntu, 'Helvetica Neue', sans-serif",};const shape = { borderRadius: spacing['xxsmall'],};export const theme = { palette, shadows, typography, shape,};
En el archivo de tema, hemos definido nuestro palette
, que son esencialmente los colores que usaremos en todos los componentes de nuestra biblioteca. También tenemos un shadows
objeto, donde definimos nuestros box-shadow
valores. También está el typography
objeto, que actualmente solo define nuestro fontFamily
. Finalmente, shape
se utiliza para propiedades como border-radius
. La estructura de este tema está inspirada en Material-UI .
¡A continuación, nuestro helpers.js
archivo!
export const isObjectEmpty = (obj) = { return Object.keys(obj).length === 0;};
Aquí, solo exponemos la isObjectEmpty
función, que toma un objeto y regresa true
si el objeto está vacío. Devuelve false
si tiene algún valor. Haremos uso de esta función más adelante.
Ahora que hemos revisado todos los archivos de la utils
carpeta, ¡es hora de comenzar a construir nuestros componentes!
Botones #
Los botones son uno de los componentes más utilizados en la web. Se utilizan en todas partes y pueden adoptar diferentes formas, tamaños y más.
Estos son los botones que vamos a construir en Figma .
Estas sutiles variaciones se aplicarán como propiedades de nuestro botón. Nos gustaría que los botones de nuestra biblioteca de componentes acepten propiedades como variant
, size
, enableElevation
(es decir box-shadow
, ) y color
.
Comenzando con el componente de botón, creemos una Button
carpeta, donde definiremos todo lo relacionado con los botones, como se discutió anteriormente.
Creemos nuestro componente de botón:
import styled from '@emotion/styled';import isPropValid from '@emotion/is-prop-valid';const StyledButton = () = {};const IGNORED_PROPS = ['color'];const buttonConfig = { shouldForwardProp: (prop) = isPropValid(prop) !IGNORED_PROPS.includes(prop),};export const Button = styled('button', buttonConfig)(StyledButton);
Aquí, comenzamos configurando nuestro componente de botón con un archivo buttonConfig
. Contiene , que se utiliza para controlar las propiedades buttonConfig
que shouldForwardProp
deben reenviarse al DOM, porque propiedades como se color
muestran en el elemento renderizado de forma predeterminada.
A continuación, definamos los tamaños de nuestros botones, que usaremos en el componente del botón.
const buttonSizeProps = { small: { fontSize: fontSizes['xsmall'], padding: `${spacing['xsmall']} ${spacing['small']}`, }, medium: { fontSize: fontSizes['small'], padding: `${spacing['small']} ${spacing['medium']}`, }, large: { fontSize: fontSizes['medium'], padding: `${spacing['medium']} ${spacing['large']}`, },};
buttonSizeProps
es un mapa de nuestros valores de tamaño ( small
, medium
y large
), y devuelve fontSize
valores padding
basados en los tamaños. Para un botón pequeño, necesitaríamos una fuente pequeña con un relleno pequeño. Lo mismo ocurre con los tamaños medianos y grandes para escalarlos adecuadamente.
A continuación, definamos una función que proporcione propiedades CSS válidas basadas en la variante pasada:
const getPropsByVariant = ({ variant, color, theme }) = { const colorInPalette = theme.palette[color]; const variants = { outline: colorInPalette ? outlineVariantPropsByPalette : defaultOutlineVariantProps, solid: colorInPalette ? solidVariantPropsByPalette : defaultSolidVariantProps, }; return variants[variant] || variants.solid;};
Aquí, la getPropsByVariant
función toma las propiedades variant
, color
y theme
y devuelve las propiedades de la variante especificada; si no se especifica ninguna variante, el valor predeterminado es solid
. colorInPalette
recupera la paleta asignada al color especificado si se encuentra y undefined
si no se encuentra en nuestro theme
objeto.
En cada variante comprobamos si realmente existe una paleta para el color indicado; si no lo hacemos, usamos colores de los objetos common
y grey
de nuestro tema, que aplicaremos en defaultOutlineVariantProps
y defaultSolidVariantProps
.
A continuación, ¡definamos nuestras propiedades variantes!
const defaultSolidVariantProps = { main: { border: `1px solid ${theme.palette.grey[100]}`, backgroundColor: theme.palette.grey[100], color: theme.palette.common.black, }, hover: { border: `1px solid ${theme.palette.grey[200]}`, backgroundColor: theme.palette.grey[200], },};const defaultOutlineVariantProps = { main: { border: `1px solid ${theme.palette.common.black}`, backgroundColor: theme.palette.common.white, color: theme.palette.common.black, }, hover: { border: `1px solid ${theme.palette.common.black}`, backgroundColor: theme.palette.common.white, color: theme.palette.common.black, },};const solidVariantPropsByPalette = colorInPalette { main: { border: `1px solid ${colorInPalette.main}`, backgroundColor: colorInPalette.main, color: colorInPalette.contrastText, }, hover: { border: `1px solid ${colorInPalette.light}`, backgroundColor: colorInPalette.light, },};const outlineVariantPropsByPalette = colorInPalette { main: { border: `1px solid ${colorInPalette.main}`, backgroundColor: theme.palette.common.white, color: colorInPalette.main, }, hover: { border: `1px solid ${colorInPalette.light}`, backgroundColor: theme.palette.common.white, color: colorInPalette.light, },};
Aquí definimos las propiedades que se aplicarán a nuestro botón en función de las variantes seleccionadas. Y, como se mencionó anteriormente, defaultSolidVariantProps
usar defaultOutlineVariantProps
colores de nuestros objetos common
y grey
como respaldo para cuando el color especificado no esté en nuestra paleta o cuando no se especifique ningún color para lo que implementamos.
Por cierto, los objetos solidVariantPropsByPalette
y outlineVariantPropsByPalette
usan el color de nuestra paleta como se especifica en el botón. Ambos tienen main
propiedades hover
que diferencian los estilos predeterminado y de desplazamiento del botón, respectivamente.
El diseño del botón que hemos utilizado tiene dos variantes, que podemos consultar en el diseño de nuestra biblioteca de componentes .
A continuación, creemos nuestra StyledButton
función, que combina todo lo que hemos hecho hasta ahora.
const StyledButton = ({ color, size, variant, enableElevation, disabled, theme,}) = { if (isObjectEmpty(theme)) { theme = defaultTheme; } const fontSizeBySize = buttonSizeProps[size]?.fontSize; const paddingBySize = buttonSizeProps[size]?.padding; const propsByVariant = getPropsByVariant({ variant, theme, color }); return { fontWeight: 500, cursor: 'pointer', opacity: disabled 0.7, transition: 'all 0.3s linear', padding: buttonSizeProps.medium.padding, fontSize: buttonSizeProps.medium.fontSize, borderRadius: theme.shape.borderRadius, fontFamily: theme.typography.fontFamily, boxShadow: enableElevation theme.shadows[1], ...(propsByVariant propsByVariant.main), ...(paddingBySize { padding: paddingBySize }), ...(fontSizeBySize { fontSize: fontSizeBySize }), ':hover': !disabled { boxShadow: enableElevation theme.shadows[2], ...(propsByVariant propsByVariant.hover), }, };};
En la StyledButton
función, asignamos defaultTheme
el tema si el theme
objeto está vacío, lo que hace que sea opcional para los consumidores de nuestra biblioteca usar ThemeProvider de Emotion para poder hacer uso de la biblioteca. Asignamos fontSize
y padding
basándonos en el buttonSizeProps
objeto. Definimos varias propiedades de botón predeterminadas, como fontWeight
y cursor
, que no están vinculadas a ninguna propiedad, y también derivamos valores color
, backgroundColor
y border
basados en el resultado de propsByVariant
.
Ahora que hemos creado nuestro Button
componente, veamos cómo podemos usarlo:
Button variant="solid" color="primary" size="small" enableElevation disabled Small Outline Elevated Button/Button
Podemos comprobar cómo se ve en CodeSandbox :
That’s how to use the Button
component. We define the following properties:
- We define a variant with a
solid
value. We could have specifiedoutline
instead. If thevariant
prop isn’t provided, we would also default tosolid
. - We define
color
, with a value ofprimary
. We also supporterror
as a color value or a color from atheme
object. If thecolor
property isn’t specified, we would fall back to our default color state. - We define
size
, with a value ofsmall
. It could bemedium
(the default) orlarge
. - We define
EnableElevation
because we want somebox-shadow
on our button. We could have chosen not to use it. - Finally, we define
disabled
because we want our button to be disabled. The additional thing we do to a disabled button is reduce its opacity.
The button doesn’t need to take any property. It defaults to a solid medium-sized button.
Componente de la caja #
A box component is a container that can hold any component or HTML element. It accepts but is not limited to properties such as padding
, margin
, display
, and width
. It can also be used as a base component for some of the other components we’ll get into later.
Here’s what it looks like on Figma:
Before diving into the code, let’s not forget to create a new folder for this component.
Now, let’s create our Box
component:
import styled from '@emotion/styled';import isPropValid from '@emotion/is-prop-valid';import { spacing, theme as defaultTheme } from '../../utils';const StyledBox = ({ paddingX, paddingY, marginX, marginY, width, display, theme, ...props}) = { if (isObjectEmpty(theme)) { theme = defaultTheme; } const padding = spacing[props.padding]; let paddingTop = spacing[props.paddingTop]; let paddingRight = spacing[props.paddingRight]; let paddingBottom = spacing[props.paddingBottom]; let paddingLeft = spacing[props.paddingLeft]; if (paddingX) { paddingLeft = spacing[paddingX]; paddingRight = spacing[paddingX]; } if (paddingY) { paddingTop = spacing[paddingY]; paddingBottom = spacing[paddingY]; } let margin = spacing[props.margin]; let marginTop = spacing[props.marginTop]; let marginRight = spacing[props.marginRight]; let marginBottom = spacing[props.marginBottom]; let marginLeft = spacing[props.marginLeft]; if (marginX) { marginLeft = spacing[marginX]; marginRight = spacing[marginX]; } if (marginY) { marginTop = spacing[marginY]; marginBottom = spacing[marginY]; } return { padding, paddingTop, paddingRight, paddingBottom, paddingLeft, margin, marginTop, marginRight, marginBottom, marginLeft, width, display, fontFamily: theme.typography.fontFamily, };};const IGNORED_PROPS = ['display', 'width'];const boxConfig = { shouldForwardProp: (prop) = isPropValid(prop) !IGNORED_PROPS.includes(prop),};export const Box = styled('div', boxConfig)(StyledBox);
The spacing
rule we defined earlier is being applied to both padding and margin, as we can see in the Box
component. We receive contextual values for padding and margin, and we look up their actual values from the spacing
object.
We accept paddingX
and paddingY
props to update padding across the horizontal and vertical axis, respectively. We do the same for marginX
and marginY
as well.
Also, we don’t want the display
and width
props to get forwarded to the DOM because we only need them in CSS. So, we add them to our list of props to ignore, and pass that on to our config.
Here’s how we could use the Box
component:
Box padding="small" paddingTop="medium" paddingBottom="medium" Simple Box Component/Box
We can see what this looks like on CodeSandbox.
In this Box
component, we’ve assigned small
as a value to our padding
property, and medium
to the paddingTop
and paddingBottom
properties. When rendered, the Box
component will have its padding-left
and padding-right
properties set to 12px
each, and its padding-top
and padding-bottom
properties set to 20px
. We could have replaced paddingTop
and paddingBottom
with paddingY
and gotten the same result.
Componente de columnas #
The Columns
component is a variation of our Box
component, with a display
type of flex
and with children spaced evenly across the x-axis.
Here is a representation of the Columns
component in Figma:
Let’s build our Columns
component!
import React from 'react';import { Box } from '../Box';export const Columns = ({ children, space, ...props }) = { return ( Box display="flex" {...props} {React.Children.map(children, (child, index) = { if (child.type !== Box) { console.warn( 'Each child in a Columns component should be a Box component' ); } if (index 0) { return React.cloneElement(child, { marginLeft: space, width: '100%', }); } return React.cloneElement(child, { width: '100%' }); })} /Box );};
We’re using React.Children to map over the Columns
component’s children. And we’re adding marginLeft
and width
properties to each of the children, except the first child, which doesn’t need a marginLeft
property because it’s the leftmost child in the column. We expect each child to be a Box
element to ensure that the necessary styles are applied to it.
Here’s how we could use the Columns
component:
Columns space="small" Box Item 1/Box Box Item 2/Box Box Item 3/Box/Columns
We can see what that looks like on CodeSandbox.
The Columns
children here are spaced evenly across the x-axis by 12 pixels because that’s what the value of small
resolves to, as we’ve defined earlier. Because the Columns
component is literally a Box
component, it can take in other Box
component properties, and we can customize it as much as we want.
Componente de pila #
This is also a variation of our Box
component that takes the full width of the parent element and whose children are spaced evenly across the y-axis.
Here is a representation of the Stack
component in Figma:
Let’s build our Stack
component:
import React from 'react';import { Box } from '../Box';import { Columns } from '../Columns';const StackChildrenTypes = [Box, Columns];const UnsupportedChildTypeWarning = 'Each child in a Stack component should be one of the types: Box, Columns';export const Stack = ({ children, space, ...props }) = { return ( Box {...props} {React.Children.map(children, (child, index) = { if (!StackChildrenTypes.includes(child.type)) { console.warn(UnsupportedChildTypeWarning); } if (index 0) { return React.cloneElement(child, { marginTop: space }); } return child; })} /Box );};
Here, we map over each child with React.Children
and apply a paddingTop
property to it with the value of the space
argument. As for the first child, we need it to take its original position, so we skip adding a marginTop
property to it. We also accept each child to be a Box
so that we can apply the necessary properties to it.
Here’s how we could use the Stack
component:
Stack space="small" Box marginTop="medium" Item 1/Box Box Item 2/Box Box Item 3/Box/Stack
We can see what that looks like on CodeSandbox.
Here, the Box
elements are spaced evenly with the small
unit, and the first Box
takes a separate marginTop
property. This shows that you can customize components however you wish.
Conclusión #
We’ve gone through the basics of using Emotion to create components in React using the APIs that it provides. This is just one of many ways to go about building a component library. There are some nuances to building it for a brand because you might not have to take theming and some other things into consideration. But if you plan to release the library to the public one day, then you’ll have to deal with requests for those missing pieces, so consider that possibility and make the library a little flexible ahead of time.
If you have any questions, feel free to drop them as comments.
The repository for this article is on GitHub, and the button designs we’ve used are on Figma.
Referencias #
- “On Building Component Libraries”, Mark Perkins, Clearleft
- “Exploring Responsive Type Scales”, Joseph Mueller
- “ Diseñar sistemas con React y Storybook ”, Emma Bostian, Frontend Masters
- Documentación oficial de emoción.
Útiles bits de front-end y UX, entregados una vez por semana.
Con herramientas que le ayudarán a realizar mejor su trabajo. Suscríbase y obtenga el PDF de las listas de verificación de diseño de interfaz inteligente de Vitaly por correo electrónico.
En front-end y UX . Con la confianza de más de 207.000 personas.
(ks, ra, al, il)Explora más en
- API
- Reaccionar
- javascript
Deja un comentario