Skip to content
This page has been auto-translated and may contain errors.View in English

Props de renderizado y componentes headless

Todos los componentes que hemos visto hasta ahora tenían una cara: renderizan algo que puedes ver. Un componente headless no tiene UI con estilos propios. Existe únicamente para proporcionar comportamiento, y renderiza cualquier hijo que le pases. Suena abstracto hasta que te das cuenta de cuántos widgets son el mismo comportamiento con diferentes formas: una estrella favorita, un menú desplegable, un switch de modo oscuro y una sección "mostrar más" son todos un booleano y una función toggle. Un componente Toggle headless captura esa lógica una sola vez, y cada widget se convierte en marcado compuesto alrededor de ella.

Este capítulo sigue el curso construyendo ese Toggle como una familia de componentes compuestos coordinados a través de context, y luego choca contra la pared que lleva a la segunda idea del capítulo: props de renderizado, el patrón para exponer el estado interno de un componente a quien lo renderiza.

Una familia Toggle headless

Toggle es propietaria de un booleano y lo proporciona, junto con una función toggle, a través de un proveedor ToggleContext con alcance. El resto de la familia consume ese context, y cada pieza cuelga de Toggle como una propiedad (Toggle.Button = ToggleButton), la sintaxis de punto de componentes compuestos:

jsx
<Toggle>
  <Toggle.Button>
    <Star />
  </Toggle.Button>
  <Toggle.On>El toggle está activado</Toggle.On>
  <Toggle.Off>El toggle está desactivado</Toggle.Off>
</Toggle>
  • Toggle.Button renderiza sus hijos en un <button type="button"> con un onClick que invierte el estado. Los hijos no necesitan sus propios manejadores de clic; un clic en cualquier cosa dentro se propaga hacia arriba al envoltorio, que es la propagación de eventos haciendo el trabajo, a través del sistema de eventos sintéticos de React en lugar de un listener en el botón mismo. Un <div> clickeable se comportaría igual para usuarios de ratón mientras pierde la activación por teclado y el anuncio del lector de pantalla, como cubre accesibilidad.
  • Toggle.On renderiza sus hijos cuando el estado es true, y null en caso contrario. Toggle.Off hace lo opuesto.

Los iconos de estrella, el marcado del menú, las etiquetas, todo viene del llamador. Eso es lo que hace que el componente sea reutilizable en widgets que no se parecen en nada: el curso coloca el mismo Toggle bajo tanto la estrella como el menú, y las piezas propias del menú envuelven las piezas de Toggle internamente para que la persona que usa <Menu> nunca vea la maquinaria.

Permitir que el exterior escuche

Un botón de estrella real no solo se repinta; le dice a un servidor que alguien lo clickeó. Entonces el componente headless necesita su propia prop event-listener, con la misma forma que onClick en tus propios componentes del capítulo children y composition. Toggle acepta una función onToggle y la ejecuta desde un effect cada vez que el estado cambia:

jsx
function Toggle({ children, onToggle = () => {} }) {
  const [on, setOn] = useState(false)
  const toggle = () => setOn(prevOn => !prevOn)
  const firstRender = useRef(true)

  useEffect(() => {
    if (firstRender.current) {
      firstRender.current = false
    } else {
      onToggle()
    }
  }, [on])

  // el proveedor y los hijos se renderizan abajo
}

Dos pequeños detalles de artesanía se esconden allí. El ref protege contra la primera ejecución del effect: los effects se ejecutan después del renderizado inicial también, y anunciar un "toggle" que nadie realizó es un bug, entonces el ref rastrea si esto todavía es el primer renderizado sin disparar re-renderizados como lo haría el estado. Y el valor por defecto = () => {} es una función noop, así que un llamador al que no le importa el evento no rompe el componente cuando llama a onToggle() de todas formas.

El array de dependencias contiene solo on. Agregar onToggle, que es lo que exhaustive-deps pide, re-ejecutaría el effect cada vez que el llamador pasa una función inline fresca, entonces este es uno de los lugares donde la regla del lint y la intención no concuerdan. Una arista en la guardia también aparece en desarrollo. StrictMode ejecuta cada effect dos veces al montar, que consume el ref en la primera ejecución y permite que el callback se dispare en la segunda. Las compilaciones de producción no hacen esto, y la solución duradera es derivar el comportamiento del cambio mismo, comparando on contra el valor anterior, en lugar de si esto es el primer renderizado.

Props de renderizado: sacando el interior

Ahora el patrón choca contra una pared. Estiliza una caja con una transición CSS en su color de fondo, renderiza la versión rellena dentro de Toggle.On y la versión vacía dentro de Toggle.Off, y la transición nunca se ejecuta. La razón: Toggle.On y Toggle.Off montan y desmontan sus hijos. React no está cambiando una clase en un elemento; está removiendo un elemento e insertando uno diferente, y CSS no puede hacer transición en un elemento que dejó de existir. Lo que el llamador necesita es el estado on mismo, para que un elemento persistente pueda variar su propio nombre de clase. El estado está atrapado dentro de Toggle.

La salida es un movimiento familiar de JavaScript: los callbacks invierten el control. Cuando llamas a addEventListener("click", callback), proporcionas la función, pero el navegador la llama y el navegador decide qué recibe (el objeto evento). Un componente puede ofrecer el mismo trato. Pásale una función, y el componente llamará a esa función con su estado interno y renderizará lo que la función devuelva:

jsx
function ToggleDisplay({ children }) {
  const { on } = useContext(ToggleContext)
  return children(on)
}

Toggle.Display = ToggleDisplay
jsx
<Toggle.Display>
  {on => <div className={`box ${on ? 'filled' : ''}`} />}
</Toggle.Display>

Los hijos de Toggle.Display no son elementos esta vez; son una función. Toggle.Display la llama, pasa on, y devuelve el JSX que obtiene. Ahora el div es un elemento que sobrevive cada toggle, solo su clase cambia, y la transición se ejecuta. Este es el patrón de render props: una prop cuyo valor es una función que el componente llama para saber qué renderizar. Algunas APIs utilizaban una prop literal llamada render, que es de dónde viene el nombre del patrón; pasar la función como children es la forma que se mantuvo.

Los props de renderizado resuelven el compartir estado entre un componente y su llamador; los custom hooks resuelven el compartir estado entre funciones planas, y después de que los hooks llegaron el ecosistema movió la mayoría de las APIs de render props. Un hook useToggle() devuelve on y toggle sin componente extra, sin anidamiento en el árbol, y sin indirección de función-como-hijos, que es por qué las librerías que alguna vez enviaban render props al estilo <Downshift> ahora envían hooks al estilo useSelect.

Los props de renderizado aún se ganan su lugar donde el proveedor necesita ser un componente: cuando el valor expuesto está vinculado a un elemento renderizado específico (medir, posicionar), o cuando una librería quiere controlar qué se renderiza dentro de un componente de límite, uno que atrapa un error de renderizado debajo o sostiene un estado de carga y decide qué se muestra en su lugar. Reconoce el patrón a primera vista, úsalo cuando el límite del componente mismo importa, y prefiere un hook cuando no lo hace.

Una nota estructural sobre Toggle.Display: llamar a children(on) durante el renderizado significa que Toggle.Display reinvoca esa función y re-renderiza su valor de retorno en cada toggle de on. Ese es el punto, y también es el costo; un render prop es una suscripción, y todo dentro de la función se re-renderiza con el estado al que se suscribe. Mantén el cuerpo de la función pequeño y empuja los subárboles pesados fuera de ella cuando no dependan del valor.

JunoComportamiento sin cara Un componente headless es uno que hace algo sin mostrar nada propio: un Toggle que mantiene un registro de activado y desactivado, mientras tú proporcionas lo que envuelve. Sus ayudantes renderizan tu contenido cuando el estado está activado o desactivado.

Y cuando necesitas el valor verdadero-o-falso en tu lado, pasas al componente una función como su hijo; el componente llama a tu función con el valor, y lo que tu función devuelve es lo que aparece en la página.

JunoComportamiento sin cara Construye componentes headless como una familia compuesta sobre un context con alcance: un padre que es propietario del estado, una pieza Button que lo invierte a través de un manejador de clic envolvente, piezas On y Off que renderizan condicionalmente hijos. Expone cambios con una prop de evento como onToggle, con valor por defecto noop y disparada desde un effect con guardia de ref del primer renderizado.

Cuando un llamador necesita el estado bruto, dale una pieza Display que llame a children como una función con el estado, que es el patrón de props de renderizado.

JunoComportamiento sin cara Los componentes headless separan el comportamiento del marcado para que una lógica sirva a widgets visualmente no relacionados; piezas de renderizado condicional como On y Off remontan sus hijos, entonces cualquier cosa que necesite continuidad, transiciones CSS incluidas, requiere exponer el estado en su lugar, a través de un prop de renderizado.

Trata los props de renderizado como inversión de control con JSX y como una suscripción con costo de re-renderizado, y prefiere un custom hook para el mismo trabajo a menos que el límite del componente mismo lleve significado.

Próximo: Custom hooks en práctica, donde la misma lógica se mueve a una función y el componente desaparece.