Saltar al contenido

Crear un popup de Chrome con React y TypeScript en 2026

React compensa cuando el popup tiene estado dinámico suficiente para justificarlo.

Extensiones de Chrome3 min de lectura

Ingeniero de software. Desarrollo extensiones de Chrome, aplicaciones web y herramientas de IA aplicada.

Contenido

Un popup con tres elementos y una llamada a una API puede ser más sencillo con TypeScript sin React: menos dependencias y menos piezas que depurar.

Cuando aparecen listas, formularios, renderizado condicional y estado que cambia, React empieza a compensar.

Esta guía explica cuándo usarlo, cómo configurarlo con CRXJS y cómo conectar el popup con la extensión mediante tipos y mensajes.

Necesitas el proyecto del tutorial inicial de TypeScript y conocer los mensajes entre componentes.

Cuándo merece la pena usar React

Suele ayudar si:

  • Hay listas dinámicas o elementos que aparecen según el estado.
  • Tienes varios formularios, interruptores o pestañas.
  • Los cambios de almacenamiento deben actualizar la interfaz.
  • Estás construyendo un panel de datos o ajustes.

Puede sobrar si:

  • La información es casi estática.
  • Solo haces uno o dos cambios en el DOM al abrir.
  • Es una utilidad pequeña, como un conversor.
  • El tamaño del paquete es una restricción importante.

Medir el tamaño del paquete

React y ReactDOM añaden peso, pero la cifra depende de sus versiones y de la compilación. Mide la salida de producción completa, incluidas imágenes y otros recursos, antes de decidir qué dependencia es el problema.

Si necesitas reducirla, evalúa Preact y su capa de compatibilidad. Compara compilaciones reales en lugar de asumir un tamaño fijo.

Configuración con CRXJS y Vite

Partiendo del proyecto básico:

npm install react react-dom
npm install -D @types/react @types/react-dom @vitejs/plugin-react

Actualiza vite.config.ts:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { crx } from "@crxjs/vite-plugin";
import manifest from "./manifest.json";

export default defineConfig({
  plugins: [react(), crx({ manifest })],
});

Después, tsconfig.json:

{
  "compilerOptions": {
    "jsx": "react-jsx",
    "types": ["chrome-types"]
  }
}

El punto de entrada del popup

Crea src/popup.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="./popup.tsx"></script>
  </body>
</html>

Y src/popup.tsx:

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./components/App";

createRoot(document.getElementById("root")!).render(
  <StrictMode>
    <App />
  </StrictMode>
);

Conectar el almacenamiento con el estado de React

Un hook puede leer el ajuste inicial y escuchar sus cambios:

// src/hooks/useStorage.ts
import { useState, useEffect } from "react";
import { syncStorage } from "../storage/typedStorage";
import type { SyncStorageSchema } from "../storage/schema";

export function useStorageValue<K extends keyof SyncStorageSchema>(
  key: K
): [SyncStorageSchema[K] | null, (value: SyncStorageSchema[K]) => Promise<void>] {
  const [value, setValue] = useState<SyncStorageSchema[K] | null>(null);

  useEffect(() => {
    // Load initial value
    syncStorage.get(key).then(setValue);

    // Listen for changes from other components
    const listener = (
      changes: Record<string, chrome.storage.StorageChange>,
      area: string
    ) => {
      if (area === "sync" && key in changes) {
        setValue(changes[key as string].newValue);
      }
    };
    chrome.storage.onChanged.addListener(listener);
    return () => chrome.storage.onChanged.removeListener(listener);
  }, [key]);

  const update = async (newValue: SyncStorageSchema[K]) => {
    await syncStorage.set(key, newValue);
    setValue(newValue); // Optimistic update
  };

  return [value, update];
}

El hook:

  • Carga el valor al montar el componente.
  • Escucha cambios procedentes del worker u otros contextos.
  • Ofrece una función que escribe en almacenamiento y actualiza el estado local.

Se apoya en el wrapper tipado de la guía de almacenamiento.

Uso:

// src/components/App.tsx
import { useStorageValue } from "../hooks/useStorage";

export function App() {
  const [blockedSites, setBlockedSites] = useStorageValue("blockedSites");
  const [theme, setTheme] = useStorageValue("theme");

  if (blockedSites === null) return <div>Loading...</div>;

  return (
    <div style={{ width: 360, padding: 16 }}>
      <h2>Blocked Sites</h2>
      <ul>
        {blockedSites.map((site) => (
          <li key={site}>{site}</li>
        ))}
      </ul>
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>
        Toggle Theme
      </button>
    </div>
  );
}

Enviar mensajes desde componentes de React

Usa el wrapper tipado de la guía de mensajes:

// src/components/TabInfo.tsx
import { useState, useEffect } from "react";
import { sendMessage } from "../utils/messaging";

export function TabInfo() {
  const [tabInfo, setTabInfo] = useState<{
    title: string;
    url: string;
  } | null>(null);

  useEffect(() => {
    sendMessage({ type: "GET_TAB_INFO" }).then(setTabInfo);
  }, []);

  if (!tabInfo) return <p>Loading tab info...</p>;

  return (
    <div>
      <h3>{tabInfo.title}</h3>
      <p>{tabInfo.url}</p>
    </div>
  );
}

Dimensiones y estilos del popup

Los límites de tamaño del popup son:

  • Máximo de 800 píxeles de ancho y 600 de alto.
  • Mínimo de 25 por 25 píxeles.
  • Si el contenido supera la altura, define un contenedor con desplazamiento vertical.

Establece dimensiones explícitas en el cuerpo o en la raíz:

/* src/popup.css */
body {
  width: 380px;
  min-height: 200px;
  margin: 0;
  font-family: system-ui, -apple-system, sans-serif;
}

Puedes usar módulos CSS, Tailwind u otra solución compatible con la compilación y la política de seguridad de contenido. Comprueba también el peso de los estilos generados.

Organizar los componentes

Una estructura práctica para un popup de complejidad media:

src/
├── popup.html
├── popup.tsx
├── components/
│   ├── App.tsx
│   ├── Settings.tsx
│   ├── SiteList.tsx
│   └── StatusBar.tsx
├── hooks/
│   ├── useStorage.ts
│   └── useMessage.ts
├── utils/
│   └── messaging.ts
├── storage/
│   ├── schema.ts
│   └── typedStorage.ts
├── types/
│   └── messages.ts
├── background.ts
└── content.ts

Mantén la organización sencilla. Muchos popups no necesitan enrutamiento, proveedores anidados ni otra biblioteca de estado. useState, useEffect y chrome.storage pueden cubrir sus necesidades.

Errores que conviene evitar

Actualizar por cualquier cambio de almacenamiento. Si escuchas diez claves sin filtrar, una modificación ajena puede provocar un renderizado. El hook del ejemplo filtra por clave.

Ignorar el ciclo de vida. Los componentes se montan y desmontan al abrir y cerrar el popup. Evita iniciar trabajo caro en cada montaje si puedes leer un resultado persistido. Delega las tareas de segundo plano en un contexto adecuado.

Usar window.localStorage para compartir estado con el worker. El worker no tiene acceso a esa API. Usa chrome.storage, como explico en la guía correspondiente.

Por dónde seguir

  1. Revisa qué puede acceder cada componente en la guía de permisos.
  2. Añade una función de IA con la guía de Claude.
  3. Prepara la publicación con la lista de la Chrome Web Store.

Esta nota forma parte de la serie Extensiones de Chrome: de cero a producto, donde explico lo que aprendí construyendo ResistGate y Amethyst, desde el primer popup hasta la publicación.

Un pequeño voto de confianza

¿Valió la pena leer esta nota?