Joy of React

Curso de Josh W. Comeau que estoy haciendo. Aquí iré plasmando mis notas sobre lo que voy aprendiendo.

Módulo 4

Component API Design

Es importante tratar de ubicar los componentes dentro de una jerarquia. Siendo la izquierda los ‘lego bricks’ como un botón o un avatar, y la parte de la derecha los componentes de más alto nivel, como un Dashboard o el componente App.

Para ello lo óptimo es seguir Separation of Concerns. En este ejemplo tenemos un componente que, por un lado es un ‘lego brick’, ya que un Banner es un componente de bajo nivel como un botón. Pero por otro lado también contiene lógica de negocio para comprobar si el usuario está registrado o no. Estamos tratando de hacer demasiadas cosas dentro de un mismo componente.

function Banner({ type, user, children }) {
  const backgroundColor =
    type === "success" ? "var(--color-success)" : "var(--color-error)";
 
  // Only logged in, verified users are
  // allowed to see the banner
  if (!user || user.registrationStatus === "unverified") {
    return null;
  }
 
  return (
    <div className={styles.banner} style={{ backgroundColor }}>
      {children}
    </div>
  );
}

Para seguir mejores prácticas y tratar de que los componentes tengan una única responsabilidad lo mejor sería dividir este componente en dos:

function Banner({ type, children }) {
  const backgroundColor = type === "success" ? "var(--color-success)" : "var(--color-error)";
  return (
    <div className={styles.banner} style={{ backgroundColor }}>
      {children}
    </div>
  );
}
 
function LoggedInBanner({type, user, children}) {
	if (!user || user.registrationStatus === "unverified") {
		return null
	} else {
	return (
		<Banner type={type}>
			{children}
		</Banner>
	)}
}

De esta manera por un lado tenemos un componente Banner que podemos reutilizar para cualquier otro banner, incluso aquellos no relacionados con la lógica del registro de usuarios. Y el componente LoggedInBanner que es la variante que solo devuelve un Banner si el usuario está registrado.

Esto nos ofrece la flexibilidad de, si a futuro, necesitamos otro Banner que solo aparezca cuando, por ejemplo, hay rebajas, es mucho más sencillo crear un componente BlackFridayBanner con combinar la lógica de los usuarios y las ofertas todo en un mismo componente.

Otra ventaja es que si el diseño cambia y tenemos que modificar el padding de los banners, con un simple cambio en Banner podríamos cambiar los estilos de todos los banners.

Prop Delegation

Prop Delegation hace referencia a la posibilidad de usar el operador …rest para pasar cualquier prop a un componente. Esto nos permite pasar props que no hayamos establecido manualmente.

La principal utilidad de esto es pasar atributos html como props que podamos posteriormente aplicar dentro del componente a un elemento HTML concreto. De esta manera evitamos tener que crear manualmente un prop por cada elemento HTML.

function App() {
	return (
		<Button text="Enviar" deactivated onClick={() => 'whatever'} />
	)
}
 
function Button({text, ...rest}) {
		
	return (
		<button 
		className="bg-red-500" //<-- IMPORTANTE dónde se coloca
		{...rest}
		>{text}</button>
	)
}

En este ejemplo usamos deactivated y onClick como props a pesar de no haberlos establecido manualmente. Para hacer esto usando Typescript hay más info en Obtener props de un elemento HTML

Conflicto entre props

El orden de los props afecta al resultado. Si colocamos {…rest} al principio, cualquier otro atributo HTML que coloquemos después sobreescribirá a lo que se haya enviado vía props. Si lo colocamos al final, los props sobreescribirán los atributos html que hayamos establecido en el componente

Esto puede ser útil si queremos establecer atributos por defecto, pero permitir (o no) la posibilidad de sobreescribirlos en caso de ser necesario

Forwarding Refs

Para pasar un useRef como prop a un componente hay que seguir una serie de pasos concretos.

Si pasamos un componente llamado ref a un componente que no sea un primitivo, recibiremos error.

<Slider ref={sliderRef} label='volume' /> //Esto no funciona

No funciona porque ref es una palabra reservada (al igual que key) para React y no la pasa como un prop al componente, sino que lo establece como un ref para ese componente dentro del parent.

Una forma de solucionarlo es usar otra word.

<Slider forwardedRef={sliderRef} label='volume' />
 
function Slider({forwardedRef, label, ...rest}) {
	const id = React.useId();
	return (
		<>
		<label htmlFor={id}>{label}</label>
		<input {...rest} id={id} type='range' ref={forwardedRef}/>
		</>
	)
}
 
export default Slider;

El problema es que tenemos que recordar el nombre del prop para da componente (forwardedRef en este caso). Si queremos utilizar ref independientemente de si es un componente primitivo o no, podemos hacer lo siguiente:

<Slider forwardedRef={sliderRef} label='volume' />
 
function Slider({label, ...rest}, ref) {
	const id = React.useId();
	return (
		<>
		<label htmlFor={id}>{label}</label>
		<input {...rest} ref={ref} id={id} type='range'/>
		</>
	)
}
 
export default forwardRef(Slider);

Al usar React forwardRef y especificar ref como segundo argumento para Slider, podemos recibir ref como prop desde el padre.

Polimorfismo

Supongamos que queremos crear un componente LinkButton que renderice un a o un button dependiendo de si le pasamos un prop href o no.

Podemos hacerlo de diferentes maneras:

Diferentes ramas con dos returns 👎

Funciona bien y es fácil, pero tiene duplicidades. en este caso className. Si queremos añadir props adicionales o cambiar los estilos tenemos que mantener y actualizar las dos ramas.

function LinkButton({ href, children }) {
//Si tiene href devolvemos un a
  if (href) {
    return (
      <a className={styles.button} href={href}>
        {children}
      </a>
    );
  }
//Si no lo tiene devolvemos un button
  return <button className={styles.button}>{children}</button>;
}

Polimorfismo 👍

Aquí mantenemos todo en una misma rama, más fácil de mantener y actualizar, pero variamos el tag del elemento dependiendo de si recibimos el prop href o no.

Para ello creamos una variable (en mayúscula) que establezca el tag del elemento y luego lo usamos como cualquier componente de React.

function LinkButton({ href, children }) {
	const Tag = typeof href === 'string' ? "a" : "button"
	return (
		<Tag href={href} className={styles.button}>
			{children}
		</Tag>
	)
}

Otra forma de hacerlo pasando el elemento como prop es pasar un prop (en este caso as) con el tag y luego destructurarlo y renombrarlo como Tag en el propio componente.

function App() {
  return (
    <main>
      <List as="ol">
        <li>Item 1</li>
        <li>Item 2</li>
        <li>Item 3</li>
      </List>
    </main>
  );
}
 
const VALID_TAGS = ['ul', 'ol']
 
function List({as: Tag} = 'ul') {
	if (!VALID_TAGS.includes(Tag)) {
	throw new Error(`Tag ${Tag} not valid. Expected ${VALID_TAGS}`)}
	return (
    <Tag>
      {children}
    </Tag>
  );
}

Slots

A estas alturas ya estamos acostumbrados a trabajar con el patrón de slots, principalmente con children

<IconButton>
Texto //Esto sería un slot
</IconButton>
 
function IconButton({children}) {
	return (
		<button>
			{children} //Aquí pintamos el slot children
		</button>
	)
}

Pero algo interesante a tener en cuenta es que podemos pasar otros componentes de React como un slot, bien sea dentro de children o dentro de cualquier otro prop.

import { Award } from "react-feather"; //Importamos icono de Feather
 
import IconButton from "./IconButton";
 
function App() {
	//Pasamos el componente Award como prop
  return <IconButton icon={Award}>Texto de botón</IconButton>;
}
 
//Renombramos el prop a mayúsculas para poder usarlo en JSX
function IconButton({ icon: Icon, children }) {
  return (
    <button className={styles.wrapper}>
      <span className={styles.iconWrapper}>
      //Pintamos el icono y añadimos los props que queramos
        <Icon strokeWidth={1.5} />
      </span>
      <span className={styles.childrenWrapper}>{children}</span>
    </button>
  );
}