Saltar al contenido principal
Crea elementos interactivos en tus documentos usando componentes de React y hooks directamente en archivos MDX.

Componentes en línea

Declara componentes directamente en tu archivo MDX:
export const Counter = () => {
  const [count, setCount] = useState(0)
  const increment = () => setCount(count + 1)
  const decrement = () => setCount(count - 1)

  return (
    <div>
      <button onClick={decrement}>-</button>
      <span>{count}</span>
      <button onClick={increment}>+</button>
    </div>
  )
}

<Counter />

Importar componentes

Los archivos de componentes deben estar en la carpeta /snippets/. Más información sobre fragmentos reutilizables.
Los imports anidados no son compatibles. Importa todos los componentes referenciados directamente en el archivo MDX padre.
Crea un archivo de componente en snippets/:
/snippets/color-generator.jsx
export const ColorGenerator = () => {
  const [hue, setHue] = useState(180)
  const [saturation, setSaturation] = useState(50)
  const [lightness, setLightness] = useState(50)
  const [colors, setColors] = useState([])

  useEffect(() => {
    const newColors = []
    for (let i = 0; i < 5; i++) {
      const l = Math.max(10, Math.min(90, lightness - 20 + i * 10))
      newColors.push(`hsl(${hue}, ${saturation}%, ${l}%)`)
    }
    setColors(newColors)
  }, [hue, saturation, lightness])

  const copyToClipboard = (color) => {
    navigator.clipboard
      .writeText(color)
      .then(() => {
        console.log(`Copied ${color} to clipboard!`)
      })
      .catch((err) => {
        console.error("Failed to copy: ", err)
      })
  }

  return (
    <div className="p-4 border dark:border-zinc-950/80 rounded-xl not-prose">
      <div className="space-y-4">
        <div className="space-y-2">
          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Hue: {hue}°
            <input
              type="range"
              min="0"
              max="360"
              value={hue}
              onChange={(e) => setHue(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(0, ${saturation}%, ${lightness}%), 
                  hsl(60, ${saturation}%, ${lightness}%), 
                  hsl(120, ${saturation}%, ${lightness}%), 
                  hsl(180, ${saturation}%, ${lightness}%), 
                  hsl(240, ${saturation}%, ${lightness}%), 
                  hsl(300, ${saturation}%, ${lightness}%), 
                  hsl(360, ${saturation}%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Saturation: {saturation}%
            <input
              type="range"
              min="0"
              max="100"
              value={saturation}
              onChange={(e) => setSaturation(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, 0%, ${lightness}%), 
                  hsl(${hue}, 50%, ${lightness}%), 
                  hsl(${hue}, 100%, ${lightness}%))`,
              }}
            />
          </label>

          <label className="block text-sm text-zinc-950/70 dark:text-white/70">
            Lightness: {lightness}%
            <input
              type="range"
              min="0"
              max="100"
              value={lightness}
              onChange={(e) => setLightness(Number.parseInt(e.target.value))}
              className="w-full h-2 bg-zinc-950/20 rounded-lg appearance-none cursor-pointer dark:bg-white/20 mt-1"
              style={{
                background: `linear-gradient(to right, 
                  hsl(${hue}, ${saturation}%, 0%), 
                  hsl(${hue}, ${saturation}%, 50%), 
                  hsl(${hue}, ${saturation}%, 100%))`,
              }}
            />
          </label>
        </div>

        <div className="flex space-x-1">
          {colors.map((color, idx) => (
            <div
              key={idx}
              className="h-16 rounded flex-1 cursor-pointer transition-transform hover:scale-105"
              style={{ backgroundColor: color }}
              title={`Click to copy: ${color}`}
              onClick={() => copyToClipboard(color)}
            />
          ))}
        </div>

        <div className="text-sm font-mono text-zinc-950/70 dark:text-white/70">
          <p>
            Base color: hsl({hue}, {saturation}%, {lightness}%)
          </p>
        </div>
      </div>
    </div>
  )
}
Luego impórtalo y úsalo:
import { ColorGenerator } from "/snippets/color-generator.jsx"

<ColorGenerator />

Consideraciones

  • SEO: Es posible que los motores de búsqueda no indexen completamente el contenido dinámico renderizado en el cliente.
  • Carga inicial: Los visitantes pueden ver un parpadeo antes de que los componentes se rendericen.
  • Accesibilidad: Asegúrate de que los lectores de pantalla anuncien los cambios de contenido dinámico.
  • Optimiza los arrays de dependencias: Incluye solo las dependencias necesarias en useEffect.
  • Memoriza operaciones costosas: Usa useMemo o useCallback cuando proceda.
  • Reduce los renderizados: Divide los componentes grandes en otros más pequeños.
  • Carga diferida: Difiere el renderizado de los componentes complejos hasta que sean necesarios para mejorar el tiempo de carga inicial de la página. Como el entorno sandbox de MDX no admite React.lazy ni import() dinámico, en su lugar condiciona los componentes pesados a la interacción del usuario o a su visibilidad. Consulta Diferir el renderizado de componentes pesados.

Diferir el renderizado de componentes pesados

React.lazy, Suspense y el import() dinámico no están disponibles en el sandbox de MDX. Para obtener el mismo beneficio, renderiza primero un marcador de posición ligero y monta el componente costoso solo después de que el lector lo solicite. Esto mantiene rápida la carga inicial de la página y, aun así, permite que los lectores interactúen con el componente completo. El ejemplo siguiente mantiene el ColorGenerator de la sección anterior sin montar hasta que el lector hace clic en Load color generator:
import { ColorGenerator } from "/snippets/color-generator.jsx"

export const LazyColorGenerator = () => {
  const [show, setShow] = useState(false)

  if (!show) {
    return (
      <button onClick={() => setShow(true)}>
        Load color generator
      </button>
    )
  }

  return <ColorGenerator />
}

<LazyColorGenerator />
Para diferir el renderizado hasta que el componente entre en el área visible, sustituye el botón por un IntersectionObserver configurado dentro de un useEffect. El patrón es el mismo: mantén show en false hasta que se active el disparador y, entonces, devuelve el componente pesado.