Category: JavaScript

  • Cómo elegir entre Hono, NestJS y Express en 2026

    Cómo elegir entre Hono, NestJS y Express en 2026

    Hono vs Express vs NestJS: cuál usar en 2026

    Tiempo estimado de lectura: 4 min

    Ideas clave

    • Decide por ejecución y mantenimiento: Edge/Serverless → Hono; contenedores y equipos grandes → NestJS; Express solo para legacy.
    • Hono sigue estándares Web (Fetch API) → despliegue directo en Cloudflare Workers, Deno y Bun; bundles mínimos y cold starts bajos.
    • NestJS aporta estructura, DI y patterns enterprise que facilitan gobernanza en equipos grandes.
    • Patrón recomendado: combinar Hono en el perímetro y NestJS en el core para optimizar latencia y mantenibilidad.

    Tabla de contenidos

    Resumen rápido (lectores con prisa)

    Hono es un microframework basado en la Fetch API ideal para despliegues Edge/Serverless con cold starts mínimos. NestJS es un framework estructurado con DI pensado para equipos grandes y aplicaciones empresariales. Express queda como opción legacy; para nuevo desarrollo en Node considera Fastify. Usa Hono en el perímetro y NestJS en el core cuando necesitas ambas propiedades.

    Introducción

    Hono vs Express vs NestJS: cuál usar en 2026 es la pregunta que debes resolver antes del primer commit. No es trivia: elegir mal condiciona latencia, coste, pruebas y, sobre todo, la capacidad del equipo para mantener código sano durante años.

    Si vas a desplegar en Cloudflare Workers, Deno o Bun, Hono cambia las reglas. Si tu sistema vive en Kubernetes y lo mantienen varios equipos, NestJS también cambia las reglas. Express… debería quedarse en mantenimiento.

    Hono vs Express vs NestJS: criterio de elección en 2026

    Primera regla: responde a dos preguntas concretas antes de elegir.

    • ¿Dónde se ejecutará el código? (Edge / Serverless vs contenedor)
    • ¿Quién lo mantendrá? (1–3 devs vs equipos >5)

    Hono está diseñado sobre estándares Web (Fetch API). Eso le da dos ventajas técnicas inmediatas: ejecuta sin cambios en Cloudflare Workers y Deno, y los bundles son mínimos → cold starts casi nulos. Ideal para servicios perimetrales: autenticación perimetral, proxies, transformaciones ligeras y APIs públicas de alta concurrencia.

    NestJS es lo opuesto deliberado: peso inicial mayor, pero estructura y DI que escalan en equipos grandes. Si necesitas módulos, interceptores, pipes, testing con mocks y un modelo mental homogéneo entre 10–50 ingenieros, NestJS reduce la deuda humana.

    Express sigue vivo por legacy. Técnicamente tiene problemas: acoplamiento a primitivas de Node (IncomingMessage/ServerResponse), soporte TypeScript no nativo y mala compatibilidad con runtimes Edge sin polyfills. Para proyectos nuevos, Fastify es una alternativa Node que merece consideración por rendimiento y plugins modernos.

    Performance y cold starts: cuándo importa realmente

    Si tu SLA pide latencia global baja y tus endpoints reciben picos distribuidos geográficamente, los cold starts importan. Hono arranca en milisegundos; NestJS arranca en cientos. Esa diferencia se traduce en UX y factura cuando se escala en funciones serverless.

    Ejemplo práctico:

    • API pública de alta concurrencia (CDN + Edge): Hono en Workers.
    • Sistema de facturación con colas, auditoría y scheduling: NestJS en contenedores.

    No mezcles requisitos. Si necesitas ambos, separa responsabilidades: Hono en el perímetro, NestJS en el núcleo.

    Escalabilidad de equipo y mantenibilidad

    NestJS gana por goleada en proyectos donde:

    • Múltiples equipos aportan features.
    • Necesitas contratos claros (interfaces, DTOs, guards).
    • Quieres testing coherente con DI y mocks.

    Hono exige disciplina. Sin una capa organizativa —convenios de carpeta, inyección manual, testing— terminas con endpoints inconexos. Aún así, para equipos pequeños o equipos senior que aceptan convenciones internas, Hono es una opción mantenible.

    Seguridad, observabilidad y ecosistema

    NestJS integra patterns enterprise: interceptores para logging, guards para auth, módulos para integración de colas (BullMQ), microservicios gRPC, etc. Si necesitas trazabilidad distribuida y middlewares estandarizados, te lo pone más fácil.

    Hono no te impide instrumentar trazas, pero requiere que diseñes la integración desde cero. Para infra ligera y métricas puntuales está bien; para auditoría y cumplimiento, NestJS acelera la adopción.

    Reglas prácticas para decidir (lista accionable)

    1. Despliegue Edge (Cloudflare Workers, Deno, Bun) → Hono. Docs: https://hono.dev, Cloudflare Workers: https://developers.cloudflare.com/workers
    2. Núcleo empresarial en Kubernetes → NestJS. Docs: https://docs.nestjs.com
    3. Proyecto nuevo pequeño, necesitas rendimiento en Node → Fastify sobre Express. Fastify: https://www.fastify.io
    4. Mantener app legacy en Express → parche, migración gradual a Fastify/NestJS o mantener si coste de migración es mayor.
    5. Necesitas tipado extremo end-to-end con frontend TS → Hono RPC o compartir DTOs desde NestJS.
    6. Requisito de cold starts <20ms → Hono (Edge); NestJS requiere contenedores siempre calientes.

    Patrón recomendado: combinar, no elegir a ciegas

    La arquitectura más práctica en 2026 no es monolítica en framework. Usa NestJS para la lógica de negocio, colas, trabajos en background y microservicios críticos; usa Hono para la capa perimetral, autenticación global, webhooks y endpoints que deben estar cerca del usuario.

    Ventaja: reduces coste y latencia donde importa, y mantienes previsibilidad y pruebas allí donde importa más: en el core.

    Conclusión

    Hono vs Express vs NestJS: cuál usar en 2026 no es una competición de popularidad. Es una cuestión de contexto. Si necesitas extremar latencia y desplegar en Edge, Hono gana. Si necesitas gobernanza de código, DI y un stack mantenible por equipos grandes, NestJS gana. Express sigue siendo válido solo para mantener legacy; para nuevo desarrollo, elige Fastify si trabajas exclusivamente en Node.

    Decide según ejecución y mantenimiento, no por hype. La mejor arquitectura combina herramientas: perímetro ligero (Hono) + corazón estructurado (NestJS). Eso te dará velocidad hoy y coherencia mañana.

    FAQ

    Respuesta: ¿Cuándo debo usar Hono en lugar de NestJS?

    Usa Hono cuando despliegues en runtimes Edge/Serverless (Cloudflare Workers, Deno, Bun) y necesites cold starts mínimos y alta concurrencia en endpoints públicos. Usa NestJS cuando necesites estructura, DI y gobernanza para equipos grandes.

    Respuesta: ¿Express todavía tiene sentido para proyectos nuevos?

    No para la mayoría de proyectos nuevos. Express se mantiene por legacy. Para nuevo desarrollo en Node, considera Fastify por rendimiento y plugins modernos.

    Respuesta: ¿Cómo afecta el entorno de ejecución a la elección del framework?

    El entorno define compatibilidad y cold starts. Hono está diseñado para la Fetch API y funciona en Workers/Deno/Bun sin polyfills. NestJS está pensado para contenedores y necesita procesos calientes para minimizar latencia.

    Respuesta: ¿Qué alternativas a Express recomiendan para Node moderno?

    Fastify es la alternativa recomendada por rendimiento y ecosistema de plugins. Evalúa Fastify sobre Express para proyectos nuevos en Node.

    Respuesta: ¿Puedo mezclar Hono y NestJS en la misma plataforma?

    Sí. Patrón recomendado: Hono en el perímetro (autenticación global, webhooks, endpoints edge) y NestJS en el núcleo (lógica de negocio, colas, trabajos). Separar responsabilidades reduce coste y mejora latencia.

    Respuesta: ¿Qué considerar sobre cold starts en serverless?

    Si tu SLA requiere latencias muy bajas y tienes picos geográficos, los cold starts importan. Hono puede arrancar en milisegundos en Edge; NestJS suele requerir contenedores calientes y arranques en cientos de ms.

  • Implementación de @supabase/server para Edge Functions Seguras

    Implementación de @supabase/server para Edge Functions Seguras

    npm install @supabase/server@latest npx skills add supabase/server — Cambia cómo piensas la seguridad y la infraestructura de tus Edge Functions

    Tiempo estimado de lectura: 4 min

    • Reduce boilerplate: @supabase/server entrega SupabaseContext por petición y primitives para evitar repetir validación de JWT, clientes anon/admin y CORS.
    • Seguridad declarativa: auth modes visibles por ruta ({ auth: ‘user’ }, ‘secret’, ‘none’, etc.) que simplifican auditorías.
    • Automatización para equipos: npx skills add supabase/server añade conocimiento a agentes para migraciones y scaffolds fiables.

    Hoy no es sobre instalar una dependencia. Es sobre cambiar cómo piensas la seguridad y la infraestructura de tus Edge Functions. Si ejecutas npm install @supabase/server@latest y luego npx skills add supabase/server, obtienes dos cosas: una librería que elimina el boilerplate y una “skill” para agentes de código que conoce la API, los patrones y las rutas de migración. Esto no es marketing; es ingeniería que reduce errores y acelera migraciones.

    Resumen rápido (lectores con prisa)

    Qué es: Un paquete que entrega SupabaseContext preconfigurado por petición y una skill para agentes.

    Cuándo usarlo: Al migrar o crear Edge Functions que requieran autenticación, roles o admin operations.

    Por qué importa: Reduce código repetido y errores; hace la seguridad declarativa.

    Cómo funciona: Wrapper middleware (withSupabase) o primitivas (createSupabaseContext) que validan auth y exponen clientes y claims.

    Qué hace la instalación (breve)

    npm install @supabase/server@latest

    Instala el paquete que crea un SupabaseContext preconfigurado por petición.

    npx skills add supabase/server

    Entrega ese contexto a agentes como Claude Code o Codex, para que puedan generar migraciones y scaffolds fiables.

    Fuentes y documentación

    Qué obtienes realmente: contextos y patrones repetibles

    Instalar @supabase/server no es solo añadir un helper: introduces un patrón consistente. El núcleo es el SupabaseContext que te dan los wrappers como withSupabase:

    • ctx.supabase: cliente con scope de usuario (respeta RLS)
    • ctx.supabaseAdmin: cliente con service role (operaciones privilegiadas)
    • userClaims / jwtClaims: identidad verificada
    • authMode: cómo se autenticó la petición

    Ejemplo mínimo (edge handler compatible con cualquier runtime)

    import { withSupabase } from 'npm:@supabase/server'
    
    export default {
      fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
        const { data } = await ctx.supabase.from('todos').select()
        return Response.json(data)
      }),
    }
    

    Eso es todo. Declaras auth y recibes contexto. Si la petición no tiene token válido, el middleware corta la ejecución antes de tocar tu lógica.

    Modos de autenticación: explícitos y visibles

    La seguridad deja de ser código disperso y pasa a ser una declaración:

    • { auth: 'user' } — solo usuarios con JWT válido
    • { auth: 'none' } — webhooks y health checks
    • { auth: 'secret' } — server-to-server con clave secreta
    • { auth: 'publishable' } — publishable key
    • { auth: ['user', 'secret'] } — acepta JWT o secret key

    Ventaja práctica: el modelo de seguridad de una función es legible en una línea. Auditar políticas, cumplir requisitos de cumplimiento o revisar riesgo queda mucho más simple.

    Migrar a JWT asimétricos sin dolor

    Antes de @supabase/server, migrar a las nuevas claves asimétricas significaba instalar jose, montar JWKS, exponer nuevos secretos y actualizar función por función. Con este paquete la validación asimétrica y la resolución de JWKS se gestionan internamente. Resultado: menos oportunidades de equivocarte y menos código repetido.

    Cuando necesitas control fino: createSupabaseContext y las primitivas

    No todo el mundo usa el wrapper. Si necesitas flujo imperativo o respuestas personalizadas, existe createSupabaseContext:

    import { createSupabaseContext } from 'npm:@supabase/server'
    
    export default {
      fetch: async (req) => {
        const { data: ctx, error } = await createSupabaseContext(req, { auth: 'user' })
        if (error) return Response.json({ error: error.message }, { status: error.status })
    
        const { data } = await ctx.supabase.from('todos').select()
        return Response.json(data)
      },
    }
    

    Y si tu arquitectura es aún más compleja (MCP servers, adaptadores personalizados) las primitivas core están disponibles: createAdminClient, createContextClient, resolveEnv, verifyAuth — las mismas funciones que alimentan los adaptadores oficiales.

    Hono y adaptadores: patrón simple para frameworks

    Hono fue el primer adaptador oficial. Un ejemplo con Hono muestra lo limpio que queda:

    import { withSupabase } from '@supabase/server/adapters/hono'
    import { Hono } from 'hono'
    
    const app = new Hono()
    app.get('/todos', withSupabase({ auth: 'user' }), async (c) => {
      const { supabase } = c.var.supabaseContext
      const { data } = await supabase.from('todos').select()
      return c.json(data)
    })
    export default { fetch: app.fetch }
    

    El adaptador inyecta supabaseContext en c.var y lista. Si necesitas otro framework, busca un adaptador o usa las primitivas core.

    Agentes y automatización: por qué añadir la “skill” es útil

    npx skills add supabase/server le da a un agente el conocimiento de la API y patterns. Eso permite prompts robustos:

    • “Analyze all Edge Functions and plan a migration to @supabase/server”
    • “Scaffold a Hono REST API with per-route auth for todos”
    • “Create an admin Edge Function that accepts user or secret auth and logs audits”

    Si tu equipo usa agentes para refactorizaciones o scaffolding, esta skill reduce la tasa de alucinaciones y acelera entregas.

    Criterio práctico para equipos técnicos

    • Empieza migrando endpoints críticos: aquellos con manipulación de roles o auditoría.
    • Sustituye shared utilities por withSupabase o createSupabaseContext: menos código repetido, menos errores.
    • Usa modes combinados (['user','secret']) para endpoints mixtos (frontend + cron jobs).
    • Revisa variables de entorno: SUPABASE_PUBLISHABLE_KEYS y SUPABASE_SECRET_KEYS en lugar de keys singulares.
    • Prueba con agentes para migraciones masivas, pero añade revisión humana.

    Conclusión

    npm install @supabase/server@latest y npx skills add supabase/server no son trucos de productividad; son una inversión en consistencia y seguridad. Si gestionas Edge Functions, adoptar este patrón reduce la superficie de error, acelera auditorías y libera tiempo para construir la lógica que realmente importa. Revisa la documentación en Documentación general y el repositorio en Repositorio Supabase para empezar.

    Dominicode Labs

    Para equipos que automatizan migraciones y workflows con agentes, una referencia práctica y recursos adicionales están disponibles en Dominicode Labs. Integrar estas prácticas con pipelines de automatización puede acelerar adopciones y reducir errores humanos.

    FAQ

    ¿Qué hace exactamente @supabase/server?

    Entrega un SupabaseContext preconfigurado por petición que incluye clientes (usuario y admin), claims y el modo de autenticación. También ofrece primitivas para escenarios imperativos o adaptadores de framework.

    ¿Necesito cambiar todas mis funciones para usarlo?

    No necesariamente. Puedes migrar endpoints críticos primero y sustituir utilidades compartidas por los wrappers o primitivias según prioridades del equipo.

    ¿Cómo maneja la validación de JWT asimétricos?

    La validación asimétrica y la resolución de JWKS se gestionan internamente por el paquete, evitando tener que instalar y montar jose y JWKS manualmente en cada función.

    ¿Qué diferencia hay entre withSupabase y createSupabaseContext?

    withSupabase es un wrapper middleware que inyecta contexto automáticamente y corta ejecución si la auth falla. createSupabaseContext es una primitiva imperativa útil cuando necesitas control fino sobre la respuesta o flujo.

    ¿La skill para agentes reemplaza la revisión humana?

    No. La skill reduce la tasa de alucinaciones y acelera la generación de migraciones y scaffolds, pero se recomienda revisión humana antes de despliegues críticos.

    ¿Dónde encuentro más documentación y ejemplos?

    Consulta la Documentación general y el Repositorio Supabase para ejemplos, adaptadores y guías de migración.

  • Cómo construir un agente de IA con NestJS y Claude API

    Cómo construir un agente de IA con NestJS y Claude API

    Cómo construir un agente de IA con NestJS + Claude API

    Tiempo estimado de lectura: 4 min

    • Separar cliente LLM, registro de herramientas y loop de agente para modularidad y pruebas.
    • Herramientas como contratos JSON-schema y validación antes de ejecución.
    • Agent loop controlado: límites de iteraciones, métricas y manejo de errores normalizado.
    • Producción: no bloquear peticiones HTTP, usar SSE/WebSockets y proteger PII y costes.
    • Observabilidad y testing: trazas por sesión, mocks para provider y canary releases.

    Introducción

    Saber cómo construir un agente de IA con NestJS + Claude API es lo que separa una demo interesante de una pieza de infraestructura que puedas mantener en producción. En este artículo encontrarás la arquitectura, decisiones técnicas y patrones que realmente importan cuando implementas tool-calling de Claude dentro de un backend modular y tipado como NestJS.

    Un agente no es un chatbot: es un bucle de decisión. Recibe objetivo, decide herramientas, ejecuta, observa resultados y vuelve a razonar hasta resolver la tarea o agotar límites.

    Resumen rápido (lectores con prisa)

    Qué: Un agente es un bucle de decisión que orquesta llamadas a herramientas externas desde un LLM.

    Cuándo: Cuando necesitas que un LLM interactúe con sistemas externos (DB, APIs, logs) de forma controlada.

    Por qué importa: Permite trazabilidad, testing y control de costes frente a soluciones ad-hoc.

    Cómo: Separando un provider LLM, un registro de herramientas con schemas JSON y un agente que controla el loop y límites.

    Cómo construir un agente de IA con NestJS + Claude API: arquitectura y flujo

    Ese bucle —Agent Loop— obliga a diseñar responsabilidades claras. El agente recibe un objetivo, decide qué herramienta usar, ejecuta esa herramienta, observa el resultado y repite hasta resolver la tarea o agotar límites.

    Arquitectura mínima recomendada:

    • Proveedor del cliente LLM (Anthropic) como Provider de NestJS.
    • Registro dinámico de herramientas (ToolRegistryService) que mapea nombres/JSON-schema a métodos de servicios.
    • Motor de ejecución (AgentService) que orquesta el loop y gestiona historial, stop reasons y seguridad.

    Referencias: NestJS Docs y Anthropic Tool Use

    Capa 1 — AnthropicProvider: el cliente como dependencia inyectable

    Nunca newees el cliente de Anthropic en controladores o servicios. Crea un provider que se inyecte y centralice la lógica del cliente.

    • Lee ANTHROPIC_API_KEY mediante ConfigService.
    • Envuelve lógica de retries, backoff y métricas (tokens usados, latencia).
    • Permite mockear en tests unitarios.

    Ejemplo conceptual: el provider expone sendMessage(payload) que encapsula anthropic.messages.create() y normaliza la respuesta (stop_reason, content, tool_call).

    Beneficio: centralizas control de coste y modelo (p. ej. cambiar de Claude 3.5 a otro modelo sin tocar el resto).

    Capa 2 — ToolRegistry: herramientas como contratos JSON y servicios

    Claude espera herramientas descritas por JSON-schema. En el backend conviene modelarlas y validar antes de ejecutar.

    • Definir cada herramienta con nombre, descripción y schema (tipado TypeScript).
    • Mapear cada herramienta a un método de servicio inyectable (p. ej. UsersService.getById, LogsService.append).
    • Implementar granularidad: una herramienta = una responsabilidad.

    Diseño práctico:

    • ToolRegistryService mantiene un mapa { toolName -> { schema, executor } }.
    • executor(args) valida los args contra el schema y ejecuta el método correspondiente en try/catch.

    Así el agent loop no necesita switches gigantes; resuelve métodos dinámicamente.

    Capa 3 — AgentService: el bucle, stop_reasons y límites

    El AgentService orquesta el loop de decisión, mantiene el historial y aplica límites y políticas de seguridad.

    Patrón del Agent Loop

    1. Construir messages + tools (esquemas) y llamar a Anthropic.
    2. Leer stop_reason:
      • end_turn: devolver respuesta final.
      • tool_use: extraer tool_name y arguments.
    3. Ejecutar herramienta vía ToolRegistryService y añadir tool_result al historial.
    4. Repetir hasta end_turn o alcanzar un límite de iteraciones.

    Normas prácticas

    • Límite de iteraciones (p. ej. max 5) para evitar bucles y costes excesivos.
    • Cada iteración registra métricas: tokens, latencia, herramienta invocada.
    • Normaliza errores: si la herramienta falla, devuelve { error: 'timeout' } a Claude, no throws.

    Producción: latencia, UX y seguridad

    Al pasar a producción hay que priorizar experiencia de usuario, seguridad y observabilidad.

    Latencia y UX

    • No bloquees la petición HTTP principal. Emite progreso con SSE o WebSockets: “Pensando…”, “Consultando DB…”.
    • Opcional: respuesta rápida + notificación cuando el resultado final esté listo (webhook / push).

    Seguridad y validación

    • Valida argumentos de herramientas con class-validator/schema JSON antes de ejecutar.
    • Logs sensibles: evita enviar secrets o PII a la API de Claude.
    • Rate limits y circuit breakers en el provider para proteger tu backend y controlar facturación.

    Observabilidad

    Traza cada sesión como un árbol de spans: requests al LLM, ejecuciones de herramientas, errores.

    Integra herramientas de visualización y trazabilidad como Langfuse o LangSmith para visualizar flujos y coste por sesión.

    Testing y despliegue

    • Mockea el AnthropicProvider y el ToolRegistryService en tests unitarios.
    • Tests de integración: entorno con Claude sandbox o replay de respuestas deterministas.
    • Canary releases: habilita el agente en subset de usuarios antes de producir a toda la base.

    Coste y gobernanza

    • Mide tokens por iteración; extrapola a coste por sesión.
    • Define políticas: cuándo permitir tool-calling (p. ej. solo usuarios verificados) y límites diarios.

    Qué evitar (errores comunes)

    • Herramientas “dios” que hacen todo: dificultan autorización y testing.
    • Dejar excepciones sin capturar: provoca loops rotos y mala UX.
    • No auditar llamadas: sin trazabilidad no sabrás por qué el agente falló o costó tanto.

    Cierre práctico

    Construir un agente con NestJS + Claude API no es magia, es disciplina arquitectónica. Si abstraes el cliente, modelas herramientas como contratos y controlas el bucle con límites, obtienes un sistema escalable, testeable y seguro.

    En el siguiente artículo veremos ejemplos concretos de ToolRegistryService y patrones para emitir progreso en SSE desde NestJS para mejorar la experiencia del usuario.

    Dominicode Labs

    Para continuidad en temas de automatización y agentes, consulta recursos adicionales en Dominicode Labs. Es una fuente útil para patrones, ejemplos y plantillas prácticas que complementan esta arquitectura.

    FAQ

    Respuesta: Un agente es un bucle de decisión que recibe un objetivo, decide qué herramienta invocar, ejecuta esa herramienta, observa el resultado y repite hasta completar la tarea o agotar límites.

    Respuesta: Un provider centraliza la configuración del cliente (API key, retries, métricas), facilita el mock en tests y permite cambiar de modelo o proveedor sin modificar la lógica de negocio.

    Respuesta: Las herramientas se describen con nombre, descripción y un JSON-schema (tipado TypeScript). Se valida la entrada antes de ejecutar y se mapea a funciones/executors inyectables.

    Respuesta: stop_reason indica la acción del LLM: end_turn para respuesta final o tool_use para invocar una herramienta. El agente interpreta y actúa según ese valor.

    Respuesta: No bloquear la petición HTTP principal; usar SSE o WebSockets para emitir progreso. También considerar respuestas rápidas con notificación posterior y aplicar límites de iteraciones para controlar latencia y coste.

    Respuesta: Mockear el provider y el registry en tests unitarios; usar sandbox o replays deterministas en integración; ejecutar canary releases antes de un despliegue completo.

  • Mejorando rendimiento y SEO al migrar de Angular a Next.js 16

    Mejorando rendimiento y SEO al migrar de Angular a Next.js 16

    De Angular a Next.js 16: lo que aprendí migrando un proyecto real

    Tiempo estimado de lectura: 4 min

    • Rendimiento y SEO fueron el motor: migramos por Core Web Vitals malos, TTFB lento y bundle que penalizaba conversión móvil.
    • Server-first cambia la mentalidad: Next.js 16 y React Server Components mueven carga y lógica al servidor, reduciendo JavaScript en cliente.
    • Menos boilerplate para mutaciones: Server Actions permiten llamar funciones server-side desde formularios sin endpoints REST intermedios.
    • Fricciones reales: cache, alcance de “use client” y observabilidad requieren disciplina adicional en producción.

    De Angular a Next.js 16: lo que aprendí migrando un proyecto real empezó como un problema de negocio: Core Web Vitals malos, TTFB lento y un bundle que penalizaba conversión móvil. La migración no fue una moda técnica; fue una necesidad para reducir fricción de usuario y mejorar SEO técnico. Esto marcó cada decisión técnica que tomamos.

    Resumen rápido (lectores con prisa)

    Qué es: Next.js 16 (App Router) usa React Server Components para renderizar HTML en servidor y enviar JavaScript mínimo al cliente.

    Cuándo usarlo: cuando SEO, Core Web Vitals o TTFB afectan métricas de negocio y necesitas ejecutar lógica sensible en servidor.

    Por qué importa: reduce bundle inicial, mejora TTFB y simplifica flujos de datos server-side.

    Cómo funciona (resumen): renderizado server-side con funciones asíncronas para fetch/ORM, Server Actions para llamadas server desde forms y control explícito de caché y revalidación.

    De Angular a Next.js 16: por qué no es solo “aprender otra sintaxis”

    Angular es un framework opinado para SPAs: inyección de dependencias, RxJS y templates declarativos. Next.js 16 (App Router) invierte ese paradigma con React Server Components (RSC): renderizado en servidor, HTML entregado al cliente y JavaScript mínimo para interactividad. Documentación oficial Next.js.

    La diferencia no es menor: pasas de pensar “qué corre en el cliente” a “qué debe correr en el servidor”. Ese cambio impacta performance, seguridad y la forma en que estructuras estado y dependencias.

    Tres lecciones técnicas que cambiaron nuestro código

    1) RxJS se queda fuera del camino principal

    En Angular, RxJS orquesta peticiones, eventos y sincronizaciones. Eso ofrece control fino (cancelaciones, operadores), pero añade complejidad de mantenimiento (unsubscribe, memory leaks).

    En Next.js 16, Server Components son funciones async: await fetch() o llamadas al ORM desde el servidor. La simplicidad reduce boilerplate y evita parpadeos de carga en el cliente. Ejemplo real: reemplazar múltiples subscriptions por una única llamada asíncrona en el server simplificó la lógica y redujo errores de sincronización.

    Nota práctica: para cancelaciones del lado del cliente hay que usar explícitamente AbortController; la ergonomía de RxJS no existe por defecto.

    2) La inyección de dependencias se reimagina

    El contenedor DI de Angular es una comodidad arquitectónica (services providedIn: 'root'). React/Next no tienen un DI integrado. Las alternativas que adoptamos:

    • Instancias únicas exportadas desde módulos ES6 (clientes DB, SDKs).
    • React Context solo para estado UI que vive en cliente (tema, sesión).
    • Props/Composición para inyección explícita en componentes que dependen de servicios.

    Resultado: más explicitud y trazabilidad, pero más disciplina para no propagar dependencias globales por accidente.

    3) Server Actions: menos endpoints, menos boilerplate

    Migrar formularios del flujo Angular (form → HttpClient → endpoint REST → backend) a Server Actions colapsó la cadena. En Next.js 16 puedes llamar funciones en el servidor directamente desde el form:

    export async function updateUser(formData: FormData) {
      'use server';
      const name = formData.get('name') as string;
      await db.user.update({ where: { id: session.userId }, data: { name } });
      revalidatePath('/profile');
    }

    El beneficio es claro: menos endpoints internos y menos código repetitivo. El riesgo: mezclar lógica de negocio en componentes si no separamos responsabilidades adecuadamente. Docs de Server Actions

    Fricciones reales que te van a doler en producción

    • Cache y freshness: Next.js App Router tiene capas de caché (memoization, data cache, route cache). Sin revalidate o cache: 'no-store' puedes servir datos obsoletos. Leer.
    • “use client” propagate cost: marcar un componente como cliente arrastra su subárbol y puede romper los beneficios del SSR si importas librerías pesadas.
    • Observabilidad de comportamiento: la frontera servidor/cliente exige testing más exhaustivo (end-to-end + integración server actions) y pipelines de CI que validen rendimiento.
    • Seguridad y surface area: Server Actions facilitan lógica server-side, pero exigen revisar permisos y sanitización con más rigor.

    Criterio práctico para Tech Leads: ¿vale la pena migrar?

    No migres por moda. Migra si:

    • Tu producto es público y SEO o Core Web Vitals impactan conversiones (ver métricas en web.dev/vitals).
    • El bundle inicial y TTFB están bloqueando métricas de negocio.
    • Necesitas ejecutar lógica sensible en servidor para reducir exposición o proteger IP.

    Mantén Angular si:

    • Es un dashboard interno con poca necesidad SEO.
    • El equipo domina RxJS y la arquitectura actual es sostenible.
    • El coste de migración supera el beneficio económico esperado.

    Conclusión

    La migración De Angular a Next.js 16: lo que aprendí migrando un proyecto real fue menos una reescritura técnica y más una reorganización de responsabilidades: qué corre en el servidor, cómo se inyectan dependencias y cómo se gestionan mutaciones. Next.js 16 ofrece ganancias reales en rendimiento y simplicidad operativa, pero exige disciplina (caché, límites use client, separación de responsabilidades). Si tu negocio lo justifica, la inversión devuelve rendimiento y una arquitectura más alineada con un futuro server-first. Si no, Angular sigue siendo una opción sólida y productiva.

    FAQ

    Respuesta: Migramos porque Core Web Vitals malos, TTFB lento y un bundle grande estaban afectando conversión móvil y SEO. La migración fue una decisión de negocio para reducir fricción de usuario y mejorar SEO técnico.

    Respuesta: No hay un reemplazo directo. En Next.js 16 se usa programación asíncrona en Server Components (await fetch(), llamadas al ORM) y, para cancelaciones cliente, AbortController. La lógica de orquestación que RxJS ofrecía suele simplificarse en el servidor o con patrones de composición en cliente.

    Respuesta: Server Actions son funciones que se ejecutan en el servidor y se pueden invocar desde formularios en el cliente. Reducen la necesidad de endpoints REST intermedios. Requieren separar responsabilidades para no mezclar lógica de negocio en componentes. Más detalles.

    Respuesta: Los riesgos principales son caché y freshness (servir datos obsoletos sin revalidate), el coste de marcar componentes como cliente que arrastran subárboles pesados y la necesidad de mayor observabilidad y testing para la frontera servidor/cliente.

    Respuesta: No conviene migrar si el proyecto es un dashboard interno con poca necesidad SEO, si el equipo domina la arquitectura actual o si el coste de migración supera el beneficio económico esperado.

    Respuesta: Usar instancias únicas exportadas desde módulos ES6 (clientes DB, SDKs), React Context para estado UI cliente y props/composición para inyección explícita en componentes. Esto aporta trazabilidad a costa de disciplina para evitar dependencias globales indeseadas.

  • Diferencias clave entre XMLHttpRequest y fetch() en JavaScript

    Diferencias clave entre XMLHttpRequest y fetch() en JavaScript

    ¿Cuáles son las diferencias entre XMLHttpRequest y fetch() en JavaScript y en los navegadores?

    Tiempo estimado de lectura: 3 min

    • XHR es una API veterana orientada a eventos y callbacks; fetch() es la API moderna basada en Promesas, diseñada para integrarse con Service Workers y Streams.
    • fetch() no rechaza por códigos HTTP; hay que comprobar response.ok. XHR requiere revisar status en onload.
    • Cancelación y progreso difieren: XHR tiene .abort() y upload.onprogress; fetch() usa AbortController y Streams para descarga.
    • Compatibilidad: XHR es universal (incluido IE); fetch() es moderno y puede requerir polyfill para entornos legacy.

    Introducción

    Si trabajas con la red en el navegador, tarde o temprano te encontrarás con esta pregunta: ¿Cuáles son las diferencias entre XMLHttpRequest y fetch() en JavaScript y en los navegadores? La respuesta no es solo sintaxis: es arquitectura, garantías y compromisos. Aquí tienes lo que realmente importa —con ejemplos y criterio técnico— para decidir con fundamento.

    En una línea: XHR es una API veterana basada en eventos y callbacks; fetch() es la API moderna basada en Promesas, diseñada para integrarse con Service Workers, Streams y async/await. Pero esa frase no resuelve bugs. Vamos por partes.

    Resumen rápido (lectores con prisa)

    Qué es: XHR es una API basada en eventos; fetch() es una API basada en Promesas y diseñada para la web moderna.

    Cuándo usarlo: Usa fetch() por defecto en proyectos modernos; conserva XHR si necesitas upload progress o soporte legacy sin polyfills.

    Por qué importa: Comportamientos sobre errores, cancelación y progreso difieren y pueden introducir bugs sutiles.

    Cómo funciona (breve): XHR expone estados y callbacks; fetch() devuelve Promesas y se integra con AbortController y Streams.

    Paradigma y legibilidad

    XMLHttpRequest (XHR): modelo orientado a eventos. Listeners, estados (readyState), comprobaciones manuales del status. Código más verboso y propenso a anidaciones.

    fetch(): devuelve una Promesa. Compatible con async/await. Composición y manejo más claro de flujos asíncronos.

    Ejemplo mínimo

    // XHR
    const xhr = new XMLHttpRequest();
    xhr.open('GET', '/api/data');
    xhr.onload = () => { if (xhr.status===200) console.log(xhr.responseText) };
    xhr.send();
    
    // fetch
    const res = await fetch('/api/data');
    if (!res.ok) throw new Error(res.status);
    console.log(await res.text());
    

    Manejo de errores HTTP (el detalle que rompe apps)

    fetch() no rechaza la promesa por códigos HTTP 4xx/5xx. Solo rechaza por fallos de red. Debes comprobar response.ok o response.status. XHR tampoco “lanza” un error automático: ahí el patrón habitual siempre ha sido revisar xhr.status en onload. La confusión surge al emigrar sin ajustar esa validación (documentado en MDN: Using Fetch).

    Cancelación de peticiones

    XHR: tiene .abort() en la propia instancia.

    fetch(): usa AbortController. Más explícito y reutilizable, pero añade un pequeño boilerplate.

    const controller = new AbortController();
    fetch('/api', { signal: controller.signal });
    controller.abort(); // cancela
    

    Progreso de subida (upload progress)

    Aquí XHR mantiene ventaja práctica: xhr.upload.onprogress ofrece bytes transferidos y total, ideal para barras de progreso en subidas grandes. fetch() puede manejar progreso de descarga con ReadableStream (Streams API), pero el progreso de subida no está estandarizado de forma simple en todos los navegadores. Si tu app hace uploads pesados, XHR o una librería que lo soporte sigue siendo la opción más directa.

    Streams y Service Workers

    fetch() está pensado para la web moderna: integración nativa con Service Workers y la Streams API, lo que permite estrategias offline y control fino de respuesta incrementales. XHR no tiene esa integración (spec Fetch: WHATWG spec).

    Cookies y credenciales (CORS)

    XHR: withCredentials = true para enviar cookies en peticiones cross-origin.

    fetch(): usa credentials (p. ej. credentials: 'include'). El comportamiento por defecto ha cambiado con el tiempo; sé explícito para evitar sorpresas.

    Compatibilidad y polyfills

    XHR es compatible con todos los navegadores (incluido IE). fetch() está disponible en navegadores modernos; para soporte legacy hay que polyfillear o usar librerías (ver MDN: XMLHttpRequest).

    Tabla rápida (resumen técnico)

    Característica XMLHttpRequest fetch()
    Paradigma Eventos/callbacks Promesas / async-await
    Errores HTTP Revisar status en onload Requiere response.ok
    Cancelación .abort() AbortController
    Upload progress Sí (upload.onprogress) No estandarizado
    Streams / Service Workers No
    Compatibilidad Universal (legacy) Modern browsers (polyfill posible)

    Criterio práctico: ¿cuál elegir?

    Elige fetch() por defecto en proyectos modernos. Mejor integración con async/await, Service Workers y arquitectura actual.

    Conserva XHR (o usa una librería que lo abstraiga, como Axios) si necesitas:

    • Progreso de subida preciso.
    • Soporte sin polyfills para navegadores antiguos.

    Usa abstracciones del ecosistema (Angular HttpClient, Axios) cuando necesites interceptores, retry policies o consistencia entre entornos; pero conoce las capas inferiores para depurar.

    Recursos y lecturas

    Conclusión

    No es una cuestión de “moderno contra antiguo” sino de garantías. Saber qué comportamientos esperan ambas APIs (especialmente sobre errores, cancelación y progreso) te evita bugs sutiles en producción. Elijas lo que elijas, hazlo con conocimiento: ese es el criterio que diferencia código que sobrevive a equipos y tiempo del que solo sobrevive a una urgencia.

    FAQ

    Respuesta: ¿fetch() rechaza la promesa en errores HTTP (4xx/5xx)?

    No. fetch() solo rechaza la promesa por fallos de red u errores de infraestructura. Para errores HTTP debes comprobar response.ok o response.status y manejar el flujo correspondiente.

    Respuesta: ¿Cómo cancelo una petición fetch?

    Usa un AbortController y pasa su señal en las opciones: fetch(url, { signal: controller.signal }). Llama a controller.abort() para cancelar.

    Respuesta: ¿Puedo obtener progreso de subida con fetch()?

    No de forma estandarizada en todos los navegadores. Para progreso de subida preciso sigue usando XHR (xhr.upload.onprogress) o una librería que lo soporte.

    Respuesta: ¿Necesito polyfill para fetch() en producción?

    Depende de tu público objetivo. Si necesitas soportar navegadores legacy (por ejemplo IE) tendrás que polyfillear fetch() o usar alternativas como XHR o Axios.

    Respuesta: ¿Cuál es la ventaja de fetch() con Service Workers?

    fetch() se integra nativamente con Service Workers y la Streams API, permitiendo estrategias offline, cacheo avanzado y control incremental de respuestas.

    Respuesta: ¿Qué debo usar para compatibilidad con IE?

    Usa XMLHttpRequest directamente o una librería/polyfill para fetch(). XHR es compatible sin polyfills en entornos legacy.

  • Cómo usar Map.getOrInsert() en JavaScript para mejorar tu código

    Cómo usar Map.getOrInsert() en JavaScript para mejorar tu código

    Map.getOrInsert(): el método que siempre quisiste en JavaScript

    Tiempo estimado de lectura: 4 min

    • Menos código, más intención: getOrInsert/getOrInsertComputed expresan “devuelve o crea” en una llamada atómica.
    • Mejor tipado en TypeScript 6.0: el retorno es T, no T | undefined, reduciendo aserciones peligrosas.
    • Uso práctico: útil en cachés, agrupaciones, contadores y construcción de grafos; evita inicializaciones innecesarias.

    Resumen rápido (lectores con prisa)

    Map.getOrInsert y Map.getOrInsertComputed permiten obtener un valor existente o insertar uno nuevo en una sola operación. En TypeScript 6.0, con target: "esnext" y lib: ["esnext"], estos métodos están tipados para devolver T en vez de T | undefined. Use getOrInsert cuando quiera expresar “devuelve o crea” de forma atómica; use la variante computada para evitar inicializaciones costosas salvo que sean necesarias.

    Map.getOrInsert(): el método que siempre quisiste en JavaScript (qué es y cómo funciona)

    Map.getOrInsert(): el método que siempre quisiste en JavaScript aparece en la primera línea porque no es una mejora menor: es la forma de expresar, de manera atómica y tipada, la intención que antes requería tres operaciones verbosas. TypeScript 6.0 ya expone los tipos vía esnext y la propuesta ECMAScript que la introduce ha alcanzado Stage 4. Aquí te explico qué hace, por qué importa y cómo integrarlo con criterio en código de producción.

    Qué hace y cómo funciona

    El patrón clásico —buscar, insertar si no existe, y luego usar— se repite en cachés, agrupaciones, contadores y grafos. Tradicionalmente escribíamos:

    if (!map.has(key)) map.set(key, compute());
    const v = map.get(key)!;

    Tres operaciones, doble lookup y una aserción no nula que silencia al compilador. getOrInsert y su variante perezosa getOrInsertComputed resuelven eso en una sola llamada:

    • map.getOrInsert(key, defaultValue): devuelve el valor si existe, si no inserta y devuelve el defaultValue.
    • map.getOrInsertComputed(key, () => value): ejecuta la función sólo si la clave no existía, evitando inicializaciones innecesarias y efectos secundarios.

    En TypeScript 6.0, usando target: "esnext" y lib: ["esnext"], el compilador tipa estos métodos de forma que el retorno es T (no T | undefined). El contrato refleja la intención: si llamas, obtienes un valor seguro.

    Por qué esto no es solo “azúcar sintáctico”

    Tres razones prácticas:

    1. Menos errores de tipo

    Con el patrón manual el compilador ve get() como T | undefined. Con getOrInsert obtienes T. Menos comprobaciones defensivas, menos ! peligrosos.

    2. Menos overhead y operaciones atomizadas

    Evitas doble lookup (has + get). Aunque la mejora de rendimiento es pequeña en muchos casos, en bucles grandes o inicializaciones masivas suma.

    3. Menos ruido cognitivo

    La intención queda expresada: “devuelve o crea”. El lector del código no necesita reconstruir el propósito tras tres líneas.

    Casos reales donde cambia la vida

    – Cachés con inicialización costosa: evita ejecutar la función de cálculo salvo que sea estrictamente necesario.

    – Agrupaciones: Map<string, T[]> ya no requiere crear arrays temporales.

    – Contadores y histogramas: inicializar un contador a 0 en una sola línea es más legible.

    – Construcción de grafos (listas de adyacencia): evita boilerplate en algoritmos BFS/DFS.

    Ejemplo típico

    const groups = new Map<string, User[]>();
    
    function addUser(role: string, user: User) {
      groups.getOrInsertComputed(role, () => []).push(user);
    }

    Sencillo, explícito y sin crear arrays inútiles.

    Integración práctica en repositorios TypeScript

    1. Habilita esnext en tsconfig:
      {
        "compilerOptions": {
          "target": "esnext",
          "lib": ["esnext"],
          "strict": true
        }
      }
    2. Revisa compatibilidad runtime:

      TypeScript te da la comprobación estática. La API como tal puede requerir polyfill o verificar la versión de Node/Bun/deno si no está implementada nativamente en tu motor.

    3. Refactor incremental:

      Busca utilidades internas con nombres como getOrCreate, getOrInit o computeIfAbsent. Reemplaza utilidades por el método nativo en PRs pequeños. Añade pruebas unitarias para casos bordes (excepciones en la función de cálculo, concurrencia en entornos compartidos).

    4. No abuses de getOrInsertComputed con side effects:

      La función de cálculo se invoca solo cuando hace falta, pero debe ser determinista y preferiblemente sin efectos secundarios que el mapa pueda registrar de forma incompleta si ocurre una excepción.

    Consideraciones de diseño y criterio Dominicode

    Estándar sobre conveniencia: si ya tienes utilidades internas, migrar a getOrInsert reduce mantenimiento y mejora la expresividad.

    No sustituyas estructuras lógicas: usar Map con getOrInsert no es excusa para diseños pobres. Sigue definiendo responsabilidades y límites de módulo.

    Audita y documenta: añade esta patrón a las guías internas y revisiones de código para homogenizar su uso.

    Fuentes y lectura adicional

    Adoptar Map.getOrInsert() hoy es reducir ruido, reforzar tipos y expresar intención. No es una moda: es una higiene mínima que facilita mantener bases de código grandes sin perder claridad. Si eres Tech Lead, estandariza su uso; si eres desarrollador, empieza a buscar esos utils/mapGetOrCreate.ts y reemplázalos por la API del lenguaje.

    FAQ

    Respuesta — ¿Qué devuelve getOrInsert si la clave existe?

    Devuelve el valor existente asociado a la clave. El contrato asegura que el retorno es T, no T | undefined.

    Respuesta — ¿En qué se diferencia getOrInsert de getOrInsertComputed?

    getOrInsert recibe un valor por defecto ya construido; getOrInsertComputed recibe una función que se ejecuta solo si la clave no existe, evitando costes de inicialización cuando no son necesarios.

    Respuesta — ¿Necesito un polyfill para usarlo en producción?

    Depende del runtime. TypeScript ofrece comprobación estática cuando apuntas a esnext, pero la API puede no estar presente en versiones antiguas de Node/Bun/deno; en esos casos se requiere polyfill o verificación de la versión del motor.

    Respuesta — ¿Cómo mejora el tipado en TypeScript 6.0?

    Con target: "esnext" y lib: ["esnext"], los tipos para estos métodos hacen que el retorno sea T, evitando la necesidad de aserciones (!) y reduciendo comprobaciones manuales de undefined.

    Respuesta — ¿Es seguro usarlo en entornos concurrentes?

    El método evita doble lookup, pero la seguridad en entornos concurrentes depende del runtime y del modelo de concurrencia. Añade pruebas y, si corresponde, mecanismos de sincronización en entornos compartidos.

    Respuesta — ¿Debo reemplazar todas mis utilidades internas?

    No necesariamente. Migra en PRs pequeños y cuando aporte claridad o reduzca mantenimiento. Prioriza casos donde el beneficio sea evidente (cachés, agrupaciones, contadores).

    Respuesta — ¿Qué precauciones al usar funciones con efectos secundarios?

    La función de cálculo en getOrInsertComputed debe ser determinista y preferiblemente sin efectos secundarios. Si lanza una excepción, el mapa podría quedar en un estado intermedio; maneja errores y añade pruebas para esos casos.

  • Migración a Signal Forms en Angular 22: Mejorando formularios reactivos

    Migración a Signal Forms en Angular 22: Mejorando formularios reactivos

    Signal Forms estable: el nuevo estándar de formularios en Angular 22

    Tiempo estimado de lectura: 4 min

    • Signal Forms reemplaza Observables por Signals nativos para exponer valor, validez y estados de control.
    • Mejora ergonomía y rendimiento al evitar suscripciones manuales y emitir reactividad síncrona y dirigida.
    • Encaja con una arquitectura Zoneless para re-rendering quirúrgico y menor sobrecarga en formularios complejos.
    • Migración pragmática: usar toSignal() y desacoplar validadores reduce el coste de adopción.

    Introducción

    Signal Forms estable aparece como la evolución natural que consolida el manejo de formularios basado en Signals en Angular 22. Signal Forms estable reemplaza la dependencia de RxJS en la capa de interfaz por un modelo de reactividad síncrono y explícito, alineado con la arquitectura Zoneless y Signals centrales del framework.

    La propuesta no es solo sintaxis; es un cambio de ergonomía y rendimiento. Aquí explico qué cambia, por qué importa y cómo preparar una migración pragmática en proyectos reales.

    Resumen rápido (lectores con prisa)

    Signal Forms expone estado de formularios como Signals nativos en lugar de Observables. Use Signals para leer estado y computed() para derivar valores síncronos. Migración pragmática: convertir valueChanges a Signals con toSignal() y desacoplar validadores.

    ¿Qué es Signal Forms estable y qué problema resuelve?

    Signal Forms estable expone el estado del formulario —valor, validez, touched/dirty— como Signals nativos en lugar de streams Observables. Los problemas que resuelve de forma directa:

    • Evita gestión manual de suscripciones (memory leaks).
    • Elimina desfases causados por emisiones asíncronas en validaciones cruzadas.
    • Encaja de forma nativa con una arquitectura Zoneless, donde Signals notifican de forma quirúrgica qué partes del DOM actualizar.

    Fuente y discusión activa sobre el diseño: discusiones en GitHub. Para contexto sobre Signals y reactividad en Angular: guía de Signals en Angular.

    Cambio conceptual: de observar eventos a leer estado derivado

    Con ReactiveFormsModule hoy suelen usarse propiedades como valueChanges o statusChanges (Observables). Signal Forms cambia el patrón: en lugar de suscribirte, lees un Signal o creas computed() que derive estados complejos.

    Comparativa rápida:

    // ReactiveForms (actual)
    readonly isValid$ = this.form.statusChanges.pipe(
      map(s => s === 'VALID'),
      distinctUntilChanged()
    );
    
    // Signal Forms (conceptual)
    readonly isValid = computed(() => this.form.status() === 'VALID');
    

    computed() es síncrono, no requiere teardown manual y se integra con el grafo de dependencias para reevaluar solo cuando los valores relevantes cambian.

    Validaciones cruzadas y tracking de dependencias

    Las validaciones cruzadas son donde RxJS más fricción introduce: operadores para evitar bucles, distinct checks, y micro-delays. Con Signals, el runtime realiza dependency tracking: si una validación depende de A y B, se reevaluará solo cuando A o B cambien, sin operadores adicionales ni emisiones redundantes.

    Esto reduce tanto complejidad como carga en el hilo principal en formularios con muchos campos interrelacionados (CRMs, ERPs).

    Integración con Zoneless y pipeline de rendimiento

    La llegada de Signal Forms completa el modelo Zoneless (ver: guía Zoneless). En una app Zoneless:

    • Cada actualización de control escribe en un Signal.
    • Angular identifica qué templates dependen de ese Signal.
    • Solo esos nodos se re-renderizan.

    Resultado: menos trabajo innecesario en eventos de alta frecuencia y trazas de depuración limpias (sin contaminación por Zone.js). Para contexto sobre migraciones Zoneless y Signals, consulta: guía de Signals en Angular.

    Ejemplo práctico de interoperabilidad temporal

    Antes de la estabilización completa, la forma pragmática de probar el patrón es convertir valueChanges a Signal con toSignal (rxjs-interop).

    import { toSignal } from '@angular/core/rxjs-interop';
    
    this.formValue = toSignal(this.form.valueChanges, { initialValue: this.form.value });
    
    computed(() => {
      const value = this.formValue();
      // derivaciones y validaciones sincronas aquí
    });
    

    Esto ofrece un puente entre ReactiveForms y lo que Signal Forms hará nativo.

    Guía de rxjs-interop: guía de rxjs-interop.

    Estado de la API y compatibilidad futura

    La API está en RFC y discusión, y lo más probable es que conviva con ReactiveFormsModule durante varias versiones para minimizar rupturas. El equipo de Angular apunta a compatibilidad con validadores existentes y a proporcionar herramientas de migración (schematics) en el momento del lanzamiento estable.

    Sigue las discusiones oficiales: discusiones en GitHub.

    Estrategia práctica para Tech Leads y equipos

    No reescribas todo hoy. Sí aplica estas medidas para reducir el coste de migración:

    • Desacopla la lógica de validación y transformación del FormGroup. Mantén funciones puras o servicios para reglas de negocio.
    • Introduce toSignal() donde tenga sentido para que los templates y la lógica consuman estado de forma síncrona.
    • Establece ChangeDetectionStrategy.OnPush en componentes nuevos para asegurar un modelo de render predecible.
    • Automatiza pruebas E2E que cubran flujos de validación cruzada y efectos secundarios.
    • Reserva una fase de migración por componentes: empezar por formularios sencillos y luego los complejos.

    Checklist mínimo antes de activar Signal Forms en producción

    • Lógica desacoplada (validadores fuera del FormGroup).
    • Cobertura de pruebas para validación y envíos.
    • Observabilidad en producción para comparar métricas (TTI, LCP).

    Conclusión: por qué importa para tu arquitectura

    Signal Forms estable no es solo una API nueva: es la culminación de la transición de Angular hacia reactividad explícita y rendimiento previsiblemente escalable. Para proyectos a largo plazo, representa menos boilerplate, menor riesgo de fugas de memoria y una integración natural con la estrategia Zoneless.

    Prepara tu código hoy —desacopla, prueba y adopta interoperabilidad con toSignal— y tu equipo tendrá una migración suave cuando Angular 22 estabilice Signal Forms. La mejora no será estética: será tangible en rendimiento y mantenibilidad.

    Fuentes y recursos

    FAQ

    Respuesta: ¿Qué es Signal Forms?

    Signal Forms expone el estado del formulario —valor, validez, touched/dirty— como Signals nativos en lugar de Observables, permitiendo lecturas síncronas y derivaciones con computed().

    Respuesta: ¿Cuándo debería considerar migrar a Signal Forms?

    Considéralo cuando busques reducir suscripciones manuales, eliminar desfases en validaciones cruzadas, o al adoptar una arquitectura Zoneless para mejoras de rendimiento predecible.

    Respuesta: ¿Cómo afecta Signal Forms a las validaciones cruzadas?

    El runtime de Signals realiza dependency tracking, por lo que las validaciones que dependen de múltiples campos solo se reevaluarán cuando cambien esas dependencias, evitando emisiones redundantes y operadores adicionales de RxJS.

    Respuesta: ¿Puedo mezclar ReactiveForms y Signal Forms?

    Sí. Antes de la estabilización completa, una estrategia pragmática es convertir valueChanges a Signal con toSignal() (rxjs-interop) para interoperabilidad temporal.

    Respuesta: ¿Qué beneficios de rendimiento puedo esperar?

    Menos re-renderings innecesarios, menos trabajo en eventos de alta frecuencia y menor riesgo de fugas por manejo de suscripciones. La integración Zoneless permite re-rendering quirúrgico de los nodos que dependen de un Signal.

    Respuesta: ¿Qué precauciones antes de activar en producción?

    Asegura lógica desacoplada (validadores fuera del FormGroup), cobertura de pruebas para validación y envíos, y observabilidad en producción para comparar métricas (TTI, LCP).

  • Aprende a tipar correctamente props, hooks y contextos en TypeScript y React

    Aprende a tipar correctamente props, hooks y contextos en TypeScript y React

    TypeScript + React: cómo tipar correctamente props, hooks y contextos

    Tiempo estimado de lectura: 4 min

    • Tipado explícito evita errores silenciosos: evita atajos como as any y prefiere contratos claros.
    • Props y refs: evita React.FC, usa referencias DOM con null inicial y valores mutables con valor inicial.
    • Contextos seguros: inicializa con null y expón hooks que hagan fail-fast si se usan fuera del provider.
    • Handlers y hooks: aprovecha los tipos de React (ChangeEvent, FormEvent) y deja que TS infiera cuando sea seguro.

    ¿Quieres dejar de parchear bugs con as any y que tu base de código deje de tener sorpresas en producción? Bien. Esto es lo que realmente necesitas saber sobre TypeScript + React: cómo tipar correctamente props, hooks y contextos. No es teoría. Son patrones que evitan errores silenciosos, mejoran el autocompletado y hacen que el código sea mantenible cuando el equipo crece.

    Resumen rápido (lectores con prisa)

    Tipar React con TypeScript reduce errores en producción y mejora DX. Evita React.FC, inicializa contextos con null y valida con hooks, usa refs con null para DOM y valores iniciales para mutables, y aprovecha los tipos sintéticos de eventos de React.

    Evita React.FC: tipa los parámetros explícitamente

    React.FC fue útil en tutoriales, pero introduce problemas: children implícitos, genéricos torpes y ruido. Tipar la función es más claro y explícito.

    interface ButtonProps {
      label: string;
      onClick: () => void;
      variant?: 'primary' | 'secondary';
      children?: React.ReactNode;
    }
    
    export function Button({ label, onClick, variant = 'primary', children }: ButtonProps) {
      return <button className={`btn-${variant}`} onClick={onClick}>{children ?? label}</button>;
    }
    

    React.ReactNode cubre todo lo que necesitas para children. Punto.

    useState: deja que TS infiera cuando pueda, explícito cuando haga falta

    Si el estado empieza con un primitivo, no especifiques el tipo. Si empieza vacío y luego será un objeto, usa una unión con null.

    interface User { id: string; email: string; }
    
    const [count, setCount] = useState(0);            // OK, inferido
    const [user, setUser] = useState<User | null>(null); // OK, explícito
    

    ¿Por qué? Porque evitarás tener que castear más adelante y te proteges contra undefined al acceder a propiedades.

    useRef: dos usos, dos reglas

    useRef sirve para referencias DOM y para valores mutables que no disparan re-render. Los tipos cambian según el valor inicial.

    • DOM refs: inicializa con null y maneja optional chaining.
    • Valores mutables: inicializa con el valor y muta .current.
    const inputRef = useRef<HTMLInputElement | null>(null);
    const renderCount = useRef(0);
    
    inputRef.current?.focus();
    renderCount.current += 1;
    

    No uses as para saltarte el null check. Esa falsedad te estallará en runtime.

    Eventos del DOM: tipa cada handler

    No uses any. React expone tipos sintéticos bien definidos. Úsalos y disfruta del autocompletado.

    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
      console.log(e.target.value);
    };
    
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
      e.preventDefault();
    };
    

    Esto evita errores tontos como leer propiedades inexistentes.

    useContext: seguro, explícito y con fail-fast

    No hagas createContext({} as ThemeContext). Ese as silencia al compilador y deja el error para producción.

    Patrón correcto: contexto con null y hook personalizado que comprueba la presencia del provider.

    interface ThemeContextType { theme: 'light'|'dark'; toggle: () => void; }
    const ThemeContext = createContext<ThemeContextType | null>(null);
    
    export function useTheme() {
      const ctx = useContext(ThemeContext);
      if (!ctx) throw new Error('useTheme debe usarse dentro de ThemeProvider');
      return ctx;
    }
    

    Fail-fast: si alguien usa el hook fuera del provider, fallas rápido y el stack trace dice dónde.

    forwardRef: firma invertida, atención al tipo genérico

    La firma de forwardRef es contraintuitiva: el primer genérico es el tipo de ref, el segundo las props.

    interface InputProps { label: string }
    
    export const CustomInput = forwardRef<HTMLInputElement, InputProps>(({ label, ...props }, ref) => (
      <label>
        {label}
        <input ref={ref} {...props} />
      </label>
    ));
    CustomInput.displayName = 'CustomInput';
    

    Siempre define displayName para facilitar debugging en React DevTools.

    Tips prácticos que cambian proyectos

    • Exporta type con export type cuando sean solo contratos. Eso deja claro que no hay runtime.
    • No uses as para “callar” al compilador. Es un atajo que se vuelve deuda.
    • Si necesitas tipos genéricos en componentes, tipa explícitamente props y evita React.FC.
    • Para APIs y carga asíncrona, combina Zod (o similar) con z.infer si necesitas validación runtime y tipos derivados.

    Checklist rápido antes de push

    • ¿Contextos inicializados con null y validados por hooks? ✔
    • ¿useRef con null para DOM y con valor inicial para mutables? ✔
    • ¿Handlers con React.ChangeEvent / FormEvent? ✔
    • ¿No hay as any salvo casos documentados? ✔

    Cierra con criterio

    Tipar React no es un ejercicio académico. Es la forma más barata de prevenir fallos en producción y mejorar el DX de tu equipo. Haz estas tres cosas hoy:

    1. Revisa contextos: elimina as y añade hooks defensivos.
    2. Estándariza useRef y useState según lo explicado.
    3. Añade displayName a los componentes con forwardRef.

    Aplica esto en tu repo. Si algo rompe después, sabrás exactamente por qué. Esto no acaba aquí. Hay más patrones (componentes polimórficos, inferencia con generics, overloads en hooks) que merecen otra nota.

    FAQ

    ¿Por qué evitar React.FC?

    Porque introduce children implícitos, dificulta genéricos y añade ruido. Tipar explícitamente los parámetros es más claro y evita sorpresas.

    ¿Cuándo especificar el tipo en useState?

    No lo especifiques si el estado inicia con un primitivo (deja que TS infiera). Si el estado inicia vacío y luego será un objeto, usa una unión con null (por ejemplo User | null).

    ¿Cómo tipar correctamente useRef para DOM?

    Inicializa la ref con null y usa el tipo del elemento: useRef<HTMLInputElement | null>(null). Accede con optional chaining (inputRef.current?.focus()).

    ¿Qué hacer si alguien usa un context fuera del provider?

    Exponer un hook que haga fail-fast: si el contexto es null, lanzar un error claro (por ejemplo throw new Error('useTheme debe usarse dentro de ThemeProvider')).

    ¿Es aceptable usar as en alguna situación?

    Evita as salvo casos documentados y justificables. Usarlo para “callar” al compilador oculta problemas que aparecerán en runtime.

    ¿Cómo mejorar la validación de APIs y mantener tipos?

    Combina validación runtime con librerías como Zod y usa z.infer para derivar tipos TypeScript a partir de los esquemas de validación.

  • Cómo Angular 21 Optimiza la Asincronía Sin Zone.js

    Cómo Angular 21 Optimiza la Asincronía Sin Zone.js

    Sin Zone.js — Native Async/Await: Angular 21 y la arquitectura Zoneless

    Sin Zone.js — Native Async/Await es el cambio arquitectónico que Angular 21 trae para cerrar años de parches y trampas alrededor de la asincronía. En lugar de confiar en interceptores globales, el framework apuesta por Signals y async/await nativo, lo que redefine cómo se detectan cambios, cómo se escribe lógica asíncrona y cómo se depura una aplicación Angular a escala.

    Resumen rápido (lectores con prisa)

    Angular 21 elimina Zone.js y usa Signals como fuente de verdad. Usa async/await nativo sin envoltorios. Resultado: detección de cambios localizada, trazas de error más limpias y menos bundle inicial.

    Cuándo: migraciones planificadas por fases. Cómo: mover estado a signals y usar OnPush; auditar subscribes y callbacks externos.

    Tiempo estimado de lectura

    Tiempo estimado de lectura: 5 min

    Ideas clave

    • Signals reemplazan a Zone.js: reactividad explícita y renderizado localizado.
    • Async/await nativo: sin transpile ni parches que oculten trazas y comportamiento.
    • Mejoras medibles: bundle inicial más pequeño, trazas limpias y menos comprobaciones globales.
    • Migración requiere auditoría: refactor de suscripciones y adopción de OnPush y signals.

    Sin Zone.js — Native Async/Await: qué cambia y por qué importa

    Zone.js surgió como una solución pragmática: interceptar (monkey‑patch) APIs del navegador —setTimeout, fetch, Promises, addEventListener— para saber cuándo lanzar la detección de cambios. Funcionó, pero con costes claros: comprobaciones globales innecesarias, trazas de pila contaminadas y fricción con APIs modernas. Más aún, Zone.js no intercepta await a nivel de motor V8, lo que obligó a técnicas de transpile para mantener el comportamiento esperado.

    Angular 21 elimina esa capa. En su lugar, Signals actúan como la fuente de verdad: cuando un signal cambia, Angular calcula qué partes de la vista dependen de él y actualiza solo esas piezas. El async/await se usa tal cual lo diseñó ECMAScript —sin envoltorios ni polyfills— y el navegador ejecuta la asincronía de manera óptima.

    Documentación clave:

    Cómo funciona el modelo Zoneless en práctica

    La filosofía es simple: reactividad explícita y renderizado localizado. Un servicio realiza una llamada asíncrona con async/await, actualiza signals y la vista reacciona únicamente a esos cambios.

    Ejemplo: servicio con signals

    @Injectable({ providedIn: 'root' })
    export class UserService {
      readonly users = signal([]);
      readonly loading = signal(false);
    
      async loadUsers(): Promise {
        this.loading.set(true);
        try {
          const response = await fetch('/api/users');
          this.users.set(await response.json());
        } finally {
          this.loading.set(false);
        }
      }
    }

    Ejemplo: template con signals

    En el template, consumir signals es directo y elimina muchos patrones previos (pipes async, subscribes que mutan propiedades de clase):

    <if (loading()) {>
      <app-skeleton />
    <} @else {>
      @for (user of users()) {
        <app-user-card [user]="user" />
      }
    }

    Angular gestiona internamente las dependencias entre signals y templates; no necesitas Zone.js para “avivar” la vista.

    Beneficios técnicos medibles

    • Reducción del bundle: eliminar Zone.js suele suponer ~100 KB minificados menos en el bundle inicial, con impacto directo en LCP.
    • Trazas de error limpias: las stack traces muestran tu código, no los callbacks internos de Zone.js, lo que reduce tiempo de diagnóstico.
    • Rendimiento en runtime: eventos de alta frecuencia (scroll, mousemove) dejan de disparar comprobaciones globales; solo los cambios efectivos actualizan la UI.
    • Compatibilidad natural con APIs modernas: fetch, Web Streams, WebSockets y librerías modernas funcionan sin trampas de detección.

    Riesgos y puntos a auditar antes de migrar

    Eliminar Zone.js no es solo quitar un import. Patrones comunes que romperán incluyen:

    • Mutar propiedades de clase dentro de subscribe() esperando que la vista se refresque automáticamente.
    • Dependencia implícita en NgZone.run() para actualizaciones desde callbacks externos.
    • Componentes sin ChangeDetectionStrategy.OnPush que confían en comprobaciones globales.

    Identifica y refactoriza estos puntos antes de desactivar Zone.js.

    Estrategia práctica de migración

    Pasos prácticos recomendados para una migración segura y por fases.

    Checklist rápida

    1. Establece ChangeDetectionStrategy.OnPush en toda la base de componentes.
    2. Migrar estado de componentes a signal() y computed(). Empieza por componentes hoja.
    3. Reemplaza suscripciones RxJS que mutan estado por toSignal() (@angular/core/rxjs-interop) o por flujos que actualicen signals explícitamente.
    4. Ejecuta pruebas E2E y de accesibilidad; valida rendimiento en escenarios reales.
    5. Activa el modo zoneless (p. ej. provideExperimentalZonelessChangeDetection() o su equivalente estable) cuando el 100% del estado dependiente de la vista esté en signals.

    Checklist rápida para auditoría antes de activar zoneless:

    • Todos los componentes críticos usan OnPush.
    • No existen mutaciones de estado implícitas en subscribes.
    • CI ejecuta pruebas de integración que cubren flujos asíncronos.
    • Observabilidad en producción: trazas y métricas para comparar comportamiento pre/post migración.

    Conclusión: menos magia, más control

    Sin Zone.js — Native Async/Await no es una moda; es la materialización de un principio: reactividad explícita y alineada con las plataformas. Para equipos que priorizan rendimiento, claridad y una depuración más rápida, Angular 21 ofrece un modelo más predecible y eficiente.

    Empieza la migración por componentes críticos, automatiza las pruebas de integración y planifica la adopción en fases: la reducción de complejidad será visible y medible. En la próxima entrega publicaremos una checklist automatizable y scripts de migración para acelerar este proceso en proyectos reales.

    FAQ

    ¿Qué es exactamente lo que reemplaza a Zone.js en Angular 21?

    Signals y el uso de async/await nativo. Signals actúan como la fuente de verdad para dependencias y actualizaciones de vista.

    ¿Por qué las trazas de error mejoran sin Zone.js?

    Porque se elimina el monkey‑patching y las trampas que insertan callbacks intermedios en las stack traces; las trazas muestran código de la aplicación y no callbacks internos.

    ¿Qué patrones rompen al desactivar Zone.js?

    Patrones que mutan estado dentro de subscribe(), dependencia en NgZone.run() y componentes que esperan comprobaciones globales en lugar de OnPush y signals.

    ¿Cuánto se reduce el bundle al quitar Zone.js?

    Eliminar Zone.js suele suponer aproximadamente 100 KB minificados menos en el bundle inicial, según la observación mencionada en el artículo.

    ¿Cuál es el primer paso práctico para migrar?

    Establecer ChangeDetectionStrategy.OnPush en los componentes y mover estado a signal() y computed(), empezando por componentes hoja.

    ¿Debo activar el modo zoneless inmediatamente en producción?

    No. Activarlo cuando el 100% del estado dependiente de la vista esté en signals y después de haber ejecutado pruebas E2E y de integración que validen flujos asíncronos.
  • Soluciones efectivas para props drilling en React

    Soluciones efectivas para props drilling en React

    ¿Qué es el props drilling en React? Guía de arquitectura y soluciones

    Tiempo estimado de lectura: 4 min

    • Props drilling es pasar props a través de varios niveles que no las usan.
    • Opciones: composición, Context API o stores externos según alcance y frecuencia de cambio.
    • Decisión técnica: privilegia composición; Context para valores estables; store para coordinación entre subsistemas.

    Resumen rápido (lectores con prisa)

    Props drilling: pasar datos por componentes intermedios que no los usan. Si atraviesa pocos niveles (<3) suele estar bien; si escala, considerar composición, Context o un store externo. Elige según alcance, frecuencia de cambio y acoplamiento.

    ¿Qué es el props drilling en React?

    El props drilling en React es cuando pasas propiedades a través de múltiples niveles de componentes que no las usan, solo las retransmiten hasta el componente que sí las necesita.

    Ejemplo visual:

    App (tiene user)
    └─ Layout
    └─ Sidebar
    └─ Menu
    └─ UserProfile (usa user)

    Código mínimo:

    function App() {
      const user = { name: "Ana", avatar: "/ana.jpg" };
      return <Layout user={user} />;
    }
    
    function Layout({ user }) { return <Sidebar user={user} />; }
    function Sidebar({ user }) { return <UserProfile user={user} />; }
    function UserProfile({ user }) { return <img src={user.avatar} alt={user.name} />; }

    Layout y Sidebar no necesitan user. Solo lo llevan. Eso genera acoplamiento y ruido en el código.

    ¿Cuándo es un problema real?

    No todo props que viaja es pecado. Pasar props uno o dos niveles es totalmente aceptable. El problema aparece cuando:

    • La propiedad atraviesa tres o más niveles.
    • Múltiples props no relacionadas llenan la firma de componentes intermedios.
    • Mover un componente exige recablear docenas de firmas.
    • Los re-renders se disparan y el rendimiento cae.

    Consecuencias prácticas: acoplamiento innecesario, refactors costosos, más tests y mayor probabilidad de bugs al cambiar la forma del dato.

    Soluciones (con criterio técnico)

    No hay varita mágica. Hay herramientas y criterios para escoger la correcta.

    1) Composición de componentes (cuando aplica)

    Primera regla: intenta composición antes que librerías. Si el componente final vive en un subárbol que puedes construir desde el padre que tiene el dato, inyecta el subárbol.

    function App() {
      const user = { name: "Ana", avatar: "/ana.jpg" };
      return (
        <Layout sidebar=&{``} />
      );
    }

    Ventaja: cero dependencias, cero props intermedios. Referencia: docs de React sobre composición

    Cuándo usar: datos locales a una sección, poco compartidos fuera del subárbol.

    2) Context API (cuando el dato es global y estable)

    Context te permite proveer un valor desde arriba y consumirlo en cualquier punto del árbol, sin pasar props intermedios.

    const UserContext = React.createContext(null);
    
    function App() {
      const user = { name: "Ana" };
      return <UserContext.Provider value={user}><Layout /></UserContext.Provider>;
    }
    
    function UserProfile() {
      const user = useContext(UserContext);
      return <span>{user.name}</span>;
    }

    Docs oficiales: React Context

    Advertencia: Context es ideal para valores que cambian poco (tema, idioma, sesión). Si el valor cambia con alta frecuencia, todos los consumidores se re-renderizan y el rendimiento puede sufrir.

    3) Estado global / stores (cuando la app escala)

    Cuando múltiples partes desconectadas del árbol necesitan leer y escribir el mismo estado, un store fuera del árbol es la opción práctica.

    Opciones razonables hoy:

    • Zustand: simple, sin boilerplate, buen rendimiento.
    • Redux Toolkit: trazabilidad y patterns para apps enterprise.
    • Jotai/Recoil: atom-based state para control fino de re-renders.

    No uses un store global por moda. Úsalo cuando la composición y Context se queden cortos.

    Cómo decidir (lista rápida)

    Hazte estas preguntas antes de refactorizar:

    1. ¿Cuántos niveles atraviesa la prop? (<3 → probablemente ok)
    2. ¿Los componentes intermedios la usan? (si sí, deja el flujo)
    3. ¿El dato cambia con frecuencia? (si sí, evita Context)
    4. ¿Se comparte entre partes no relacionadas de la UI? (si sí, considera un store)

    Si la respuesta apunta a complejidad real, planifica: migración por fases, pruebas y medición de re-renders.

    Buenas prácticas finales

    • Mantén el estado lo más cerca posible del lugar donde se usa.
    • Prefiere composición cuando sea viable.
    • Usa Context para datos estables.
    • Reserva stores externos para coordinación entre subsistemas.
    • Evita micro-optimizaciones prematuras: primero estructura, luego perf.

    Referencias útiles

    Esto no acaba aquí: en el siguiente post veremos cómo migrar un árbol con props drilling a Zustand paso a paso, sin romper la app ni a los desarrolladores. Suscríbete al boletín de Dominicode para recibir la guía y los snippets listos para copiar.

    FAQ

    ¿Qué es exactamente el props drilling?

    Es el patrón donde pasas propiedades desde un componente superior hasta uno profundo, atravesando componentes intermedios que no las usan. Genera firmas de props infladas y acoplamiento innecesario.

    ¿Cuándo puedo ignorarlo?

    Cuando la prop atraviesa uno o dos niveles y no complica el mantenimiento. No todo pasaje de props requiere refactor.

    ¿Cuándo usar Context en lugar de un store?

    Usa Context para valores globales y estables (tema, idioma, sesión). Evita Context para datos que cambian con alta frecuencia o requieren escrituras concurrentes desde múltiples partes.

    ¿La composición siempre es la mejor opción?

    No siempre, pero es la primera estrategia a intentar: sin dependencias y con menor acoplamiento cuando puedes construir el subárbol desde el padre que tiene el dato.

    ¿Qué problemas de rendimiento trae Context?

    Si el valor del Provider cambia con frecuencia, todos los consumidores se re-renderizan, lo que puede impactar el rendimiento. Se puede mitigar con memos, splitting de contexts o stores que controlen re-renders finos.

    ¿Qué store elegir si la app escala?

    Depende: Zustand para simplicidad y rendimiento, Redux Toolkit para trazabilidad en enterprise, y Jotai/Recoil para control fino de re-renders.