[{"data":1,"prerenderedAt":651},["ShallowReactive",2],{"post-index":3,"post-watch-vs-watcheffect-cotizador-envios-vue-3":97},[4,15,24,32,40,49,58,66,75,82,90],{"slug":5,"title":6,"description":7,"image":8,"tags":9},"watch-vs-watcheffect-cotizador-envios-vue-3","watch vs. watchEffect en Vue 3: evita cotizaciones de envío desactualizadas","Construye un cotizador de envíos con Vue 3.5+: usa watch, debounce y onWatcherCleanup para evitar tarifas obsoletas en un checkout real.","https://a-us.storyblok.com/f/1022875/1672x941/2033377396/watch-vs-watcheffect-cotizador-envios-vue-3.png",[10,11,12,13,14],"backend","composition-api","intermediate","watch","watch-effect",{"slug":16,"title":17,"description":18,"image":19,"tags":20},"how-to-use-teleport-in-vue-3","Guía práctica de Vue 3: Cómo usar \u003CTeleport> para modales, tooltips y notificaciones adaptativas","¿Sabías que puedes mover parte del DOM de un componente Vue a otro lugar del HTML sin romper la reactividad? En esta guía aprenderás cómo usar \u003CTeleport> en Vue 3, desde lo básico hasta casos reales como notificaciones, menús contextuales y overlays.","https://a-us.storyblok.com/f/1022875/1376x768/9bbb639e1d/teleport.jpg",[21,12,22,23],"components","teleport","ui-patterns",{"slug":25,"title":26,"description":27,"image":28,"tags":29},"use-definemodel","defineModel() en Vue 3.4+: La evolución elegante del v-model en componentes","A partir de Vue 3.4 y disponible en la actual versión 3.5+, defineModel() se ha convertido en una de las macros más potentes dentro del \u003Cscript setup>. Esta nueva sintaxis te permite trabajar con v-model de forma más clara, concisa y tipada, eliminando la necesidad de declarar manualmente props y emits para la comunicación padre-hijo. Si estás trabajando con Vue 3.5+, ya deberías estar usándolo.  ¿La clave? Menos boilerplate, más fluidez, y una integración impecable con los modifiers y múltiples v-model. ¡Dile adiós a modelValue verboso y hola a bindings con estilo!","https://a-us.storyblok.com/f/1022875/1536x1024/7c8b51e1ae/definemodel2.png",[21,30,12,31],"define-model","v-model",{"slug":33,"title":34,"description":35,"image":36,"tags":37},"vue-fire"," Potencia tu app Vue con Firebase + VueFire","¿Qué es VueFire? VueFire te permite conectar tu app Vue 3 con Firebase (Firestore, Auth, RTDB, Storage) de forma sencilla y reactiva usando composables como useAuth, useFirestore, etc. Ideal si quieres apps en tiempo real sin escribir cientos de líneas de lógica.","https://a-us.storyblok.com/f/1022875/1376x768/9774bccf90/vuefire.jpg",[10,38,12,39],"firebase","vuefire",{"slug":41,"title":42,"description":43,"image":44,"tags":45},"web-components-with-modern-vue","¿Y si Vue.js también hablara Web Components? La magia de defineCustomElement","Si eres fan de Vue.js como yo, seguramente has creado componentes reutilizables una y otra vez. Pero… ¿alguna vez te preguntaste cómo compartir tus componentes Vue fuera de una app Vue? ¿Qué pasaría si pudieras usarlos en React, Angular o incluso en HTML puro? La respuesta está en los Web Components y Vue tiene una herramienta poderosa para eso: defineCustomElement.  Hoy te voy a contar cómo Vue y los Web Components pueden ser mejores amigos. Te prometo que al final vas a querer probarlo en tu próximo proyecto (o experimento).","https://a-us.storyblok.com/f/1022875/1536x1024/7b5cd5a452/web-components.png",[46,21,47,48],"advanced","define-custom-element","interoperability",{"slug":50,"title":51,"description":52,"image":53,"tags":54},"tailwind-4-integration-with-vue-cli","Guía rápida: Integrando Tailwind CSS 4 en proyectos con Vue CLI","Tailwind CSS 4 trae mejoras significativas en rendimiento y flexibilidad, pero ¿cómo integrarlo correctamente en proyectos creados con Vue CLI? En esta guía rápida, te mostraré paso a paso cómo configurar Tailwind 4 en tu proyecto Vue CLI, optimizando tu flujo de desarrollo y aprovechando al máximo sus nuevas características.","https://a-us.storyblok.com/f/1022875/1792x1024/71cb200538/vue-cli-and-tailwind-4.webp",[12,55,56,57],"styling","tailwind","vue-cli",{"slug":59,"title":60,"description":61,"image":62,"tags":63},"how-to-use-defineexpose","¿Cómo usar defineExpose?","Dentro de un componente que utiliza \u003Cscript setup>, defineExpose() es una macro del compilador que permite exponer de forma explícita variables, funciones o incluso refs para que puedan ser accesibles desde el exterior. Esto es útil, por ejemplo, cuando queremos controlar un componente a través de una referencia (ref) en el componente padre.","https://a-us.storyblok.com/f/1022875/1376x768/487e84614f/defineexpose.jpg",[64,21,11,65],"beginner","define-expose",{"slug":67,"title":68,"description":69,"image":70,"tags":71},"how-to-use-v-memo","¿Comó usar v-memo?","En el desarrollo de aplicaciones con Vue.js, especialmente cuando se trabaja con listas largas de elementos, es crucial optimizar el renderizado para garantizar un buen rendimiento. Una de las herramientas que Vue.js ofrece para este propósito es la directiva v-memo. En este artículo, se analiza un ejemplo práctico de cómo utilizar v-memo para mejorar el rendimiento en una lista de tareas y se exploran algunas mejoras adicionales que pueden aplicarse.","https://a-us.storyblok.com/f/1022875/1376x768/b989b9ebdf/v-memo-image.jpg",[72,12,73,74],"directives","performance","v-memo",{"slug":76,"title":77,"description":78,"image":79,"tags":80},"what-is-a-composable","¿Qué es un Composable?","Un composable es una función que utiliza la Composition API de Vue.js para encapsular y reutilizar lógica con estado de manera modular. En otras palabras, permite extraer fragmentos de lógica (por ejemplo, rastreo de la posición del mouse, manejo de llamadas asíncronas, etc.) para que puedan ser utilizados en varios componentes sin repetir código.","https://a-us.storyblok.com/f/1022875/1376x768/0c48bd03b4/vue-composables.jpg",[64,81,11],"composables",{"slug":83,"title":84,"description":85,"image":86,"tags":87},"use-defineasynccomponent","defineAsyncComponent en Vue 3: Implementación y casos de uso.","En Vue 3, la función defineAsyncComponent permite cargar componentes de manera asíncrona, lo que es útil para dividir aplicaciones grandes en partes más pequeñas y mejorar el rendimiento al cargar solo los componentes necesarios cuando se requieren. ","https://a-us.storyblok.com/f/1022875/1376x768/ff546597c5/vue_defineasync_img.jpg",[21,88,12,89,73],"define-async-component","lazy-loading",{"slug":91,"title":92,"description":93,"image":94,"tags":95},"tailwind-4-integration-in-vue-js-3","¿Cómo integrar Tailwind 4 en Vue.js 3 + Vite?","Tailwind CSS 4 ha llegado con una arquitectura optimizada para rendimiento y flexibilidad, ofreciendo compilaciones hasta 5 veces más rápidas y un flujo de trabajo más ágil. En este post, exploraremos cómo integrar esta nueva versión con Vue 3, aprovechando características como la detección automática de contenido, el nuevo motor de alto rendimiento, las variables CSS nativas y la compatibilidad con Vite.  Si quieres construir interfaces modernas con menos configuración y más poder, acompáñame en esta guía donde aprenderás a instalar, configurar y usar Tailwind CSS 4 en Vue 3. 🚀","https://a-us.storyblok.com/f/1022875/1376x768/626e80a702/taiwlind4_vue_3.jpg",[12,55,56,96],"vite",{"name":6,"created_at":98,"published_at":99,"updated_at":100,"id":101,"uuid":102,"content":103,"slug":5,"full_slug":642,"sort_by_date":125,"position":643,"tag_list":644,"is_startpage":121,"parent_id":645,"meta_data":125,"group_id":646,"first_published_at":647,"release_id":125,"lang":648,"path":649,"alternates":650,"default_full_slug":125,"translated_slugs":125},"2026-09-27T17:41:51.164Z","2026-09-27T18:13:32.546Z","2026-09-27T18:13:32.568Z",224630010569317,"f1984f97-f3b2-4061-b926-8aa577da645d",{"_uid":104,"demo":105,"image":115,"title":6,"content":122,"component":641,"description":7},"ea0f33f1-ea0b-4b64-8d31-b0672b8dfda2",[106],{"id":107,"alt":108,"name":109,"focus":109,"title":110,"source":111,"filename":112,"copyright":109,"fieldtype":113,"meta_data":114},224636324148935,"Animación de nueve segundos: Cusco inicia una consulta lenta, el usuario cambia a Lima y la tarifa final permanece en S/ 12.00.","","Demo animada: cotización de envío sin tarifas obsoletas","Demostración visual de El Rincón de Vue; tarifas simuladas.","https://a-us.storyblok.com/f/1022875/960x540/6d25cc1ffe/watch-vs-watcheffect-cotizador-demo.gif","asset",{"alt":108,"title":110,"source":111},{"id":116,"alt":117,"name":109,"focus":109,"title":118,"source":119,"filename":8,"copyright":109,"fieldtype":113,"meta_data":120,"is_external_url":121},224630786000576,"Ilustración de un cotizador con varias solicitudes; dos respuestas antiguas se cancelan y solo la más reciente llega al resultado.","Peticiones asíncronas y cancelación en Vue 3","Ilustración generada para El Rincón de Vue",{"alt":117,"title":118,"source":119},false,{"type":123,"attrs":124,"content":126},"doc",{"backgroundColor":125},null,[127,147,176,183,188,244,255,271,284,293,374,409,414,425,432,437,465,470,487,492,510,515,524,557,562,567,629],{"type":128,"attrs":129,"content":130},"paragraph",{"textAlign":125},[131,134,139,141,145],{"text":132,"type":133},"En un checkout, la persona elige un destino y cambia el peso del paquete. La interfaz pide una tarifa al servidor cada vez que esos datos cambian. Parece sencillo hasta que dos respuestas llegan en un orden distinto al de las solicitudes: la tarifa de ","text",{"text":135,"type":133,"marks":136},"Cusco",[137],{"type":138},"bold",{"text":140,"type":133}," tarda 900 ms y la de ",{"text":142,"type":133,"marks":143},"Lima",[144],{"type":138},{"text":146,"type":133}," tarda 120 ms. Si el usuario cambia de Cusco a Lima rápidamente, la respuesta antigua puede sobrescribir la nueva. La pantalla termina mostrando un precio para el destino equivocado.",{"type":128,"attrs":148,"content":149},{"textAlign":125},[150,152,156,158,162,164,168,170,174],{"text":151,"type":133},"Este artículo construye un cotizador pequeño pero reproducible. Usaremos ",{"text":13,"type":133,"marks":153},[154],{"type":155},"code",{"text":157,"type":133}," para declarar exactamente qué cambios disparan la consulta, un retraso corto para no consultar por cada pulsación, ",{"text":159,"type":133,"marks":160},"AbortController",[161],{"type":155},{"text":163,"type":133}," para cancelar la petición anterior y ",{"text":165,"type":133,"marks":166},"onWatcherCleanup",[167],{"type":155},{"text":169,"type":133}," para retirar también cualquier respuesta tardía. El ejemplo requiere ",{"text":171,"type":133,"marks":172},"Vue 3.5 o superior",[173],{"type":138},{"text":175,"type":133}," y funciona en una aplicación Vue con un endpoint HTTP; al final incluyo un endpoint de demostración para Nuxt.",{"type":177,"attrs":178,"content":180},"heading",{"level":179,"textAlign":125},2,[181],{"text":182,"type":133},"El problema: la última respuesta no siempre es la correcta",{"type":128,"attrs":184,"content":185},{"textAlign":125},[186],{"text":187,"type":133},"Supongamos que el peso es de 1 kg:",{"type":189,"attrs":190,"content":192},"ordered_list",{"order":191},1,[193,206,218,231],{"type":194,"content":195},"list_item",[196],{"type":128,"attrs":197,"content":198},{"textAlign":125},[199,201,204],{"text":200,"type":133},"Seleccionas ",{"text":135,"type":133,"marks":202},[203],{"type":138},{"text":205,"type":133}," y se inicia la solicitud A.",{"type":194,"content":207},[208],{"type":128,"attrs":209,"content":210},{"textAlign":125},[211,213,216],{"text":212,"type":133},"Cambias a ",{"text":142,"type":133,"marks":214},[215],{"type":138},{"text":217,"type":133}," y se inicia la solicitud B.",{"type":194,"content":219},[220],{"type":128,"attrs":221,"content":222},{"textAlign":125},[223,225,229],{"text":224,"type":133},"B responde primero: la interfaz muestra ",{"text":226,"type":133,"marks":227},"S/ 12.00",[228],{"type":138},{"text":230,"type":133},".",{"type":194,"content":232},[233],{"type":128,"attrs":234,"content":235},{"textAlign":125},[236,238,242],{"text":237,"type":133},"A responde después: sin protección, la interfaz cambia a ",{"text":239,"type":133,"marks":240},"S/ 20.00",[241],{"type":138},{"text":243,"type":133},", aunque el destino visible sigue siendo Lima.",{"type":128,"attrs":245,"content":246},{"textAlign":125},[247,249,253],{"text":248,"type":133},"El problema no es la velocidad de Vue. Es una ",{"text":250,"type":133,"marks":251},"condición de carrera",[252],{"type":138},{"text":254,"type":133}," entre operaciones asíncronas. La solución debe cancelar el trabajo anterior y comprobar que una respuesta aún pertenece a los datos actuales antes de pintarla.",{"type":177,"attrs":256,"content":257},{"level":179,"textAlign":125},[258,260,263,265,269],{"text":259,"type":133},"¿Por qué ",{"text":13,"type":133,"marks":261},[262],{"type":155},{"text":264,"type":133}," aquí y no ",{"text":266,"type":133,"marks":267},"watchEffect",[268],{"type":155},{"text":270,"type":133},"?",{"type":128,"attrs":272,"content":273},{"textAlign":125},[274,277,279,282],{"text":13,"type":133,"marks":275},[276],{"type":155},{"text":278,"type":133}," observa fuentes explícitas. Nuestro precio solo depende del destino y del peso; leer el estado de carga o el resultado dentro del callback no debe generar otra consulta. ",{"text":266,"type":133,"marks":280},[281],{"type":155},{"text":283,"type":133}," detecta automáticamente las dependencias reactivas que se leen durante su ejecución sincrónica. Es útil cuando queremos que un efecto siga todas esas dependencias, pero aquí la lista explícita expresa mejor la regla del negocio.",{"type":128,"attrs":285,"content":286},{"textAlign":125},[287,291],{"text":288,"type":133,"marks":289},"computed",[290],{"type":155},{"text":292,"type":133}," cumple otro papel: derivar un valor sin producir un efecto externo. Una llamada HTTP es un efecto, por eso usamos un watcher.",{"type":294,"content":295},"table",[296,316,336,355],{"type":297,"content":298},"tableRow",[299,308],{"type":300,"attrs":301,"content":302},"tableHeader",{"colspan":191,"rowspan":191,"colwidth":125},[303],{"type":128,"attrs":304,"content":305},{"textAlign":125},[306],{"text":307,"type":133},"Necesitas…",{"type":300,"attrs":309,"content":310},{"colspan":191,"rowspan":191,"colwidth":125},[311],{"type":128,"attrs":312,"content":313},{"textAlign":125},[314],{"text":315,"type":133},"Usa…",{"type":297,"content":317},[318,327],{"type":319,"attrs":320,"content":321},"tableCell",{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[322],{"type":128,"attrs":323,"content":324},{"textAlign":125},[325],{"text":326,"type":133},"Calcular un valor a partir de otros valores reactivos",{"type":319,"attrs":328,"content":329},{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[330],{"type":128,"attrs":331,"content":332},{"textAlign":125},[333],{"text":288,"type":133,"marks":334},[335],{"type":155},{"type":297,"content":337},[338,346],{"type":319,"attrs":339,"content":340},{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[341],{"type":128,"attrs":342,"content":343},{"textAlign":125},[344],{"text":345,"type":133},"Llamar a una API cuando cambian fuentes concretas",{"type":319,"attrs":347,"content":348},{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[349],{"type":128,"attrs":350,"content":351},{"textAlign":125},[352],{"text":13,"type":133,"marks":353},[354],{"type":155},{"type":297,"content":356},[357,365],{"type":319,"attrs":358,"content":359},{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[360],{"type":128,"attrs":361,"content":362},{"textAlign":125},[363],{"text":364,"type":133},"Ejecutar un efecto inmediato que siga automáticamente las lecturas reactivas sincrónicas",{"type":319,"attrs":366,"content":367},{"colspan":191,"rowspan":191,"colwidth":125,"backgroundColor":125},[368],{"type":128,"attrs":369,"content":370},{"textAlign":125},[371],{"text":266,"type":133,"marks":372},[373],{"type":155},{"type":128,"attrs":375,"content":376},{"textAlign":125},[377,379,383,385,388,390,394,398,402,404,407],{"text":378,"type":133},"Hay un detalle importante: en un callback ",{"text":380,"type":133,"marks":381},"async",[382],{"type":155},{"text":384,"type":133}," de ",{"text":266,"type":133,"marks":386},[387],{"type":155},{"text":389,"type":133},", las lecturas reactivas realizadas ",{"text":391,"type":133,"marks":392},"después del primer ",[393],{"type":138},{"text":395,"type":133,"marks":396},"await",[397],{"type":155},{"text":399,"type":133,"marks":400}," no se registran como dependencias",[401],{"type":138},{"text":403,"type":133},". Para este cotizador, declarar las fuentes con ",{"text":13,"type":133,"marks":405},[406],{"type":155},{"text":408,"type":133}," evita esa ambigüedad.",{"type":177,"attrs":410,"content":411},{"level":179,"textAlign":125},[412],{"text":413,"type":133},"El componente completo",{"type":128,"attrs":415,"content":416},{"textAlign":125},[417,419,423],{"text":418,"type":133},"El endpoint devolverá ",{"text":420,"type":133,"marks":421},"{ amount, currency, etaDays }",[422],{"type":155},{"text":424,"type":133},". En una aplicación real, el servidor consultaría la transportadora o aplicaría sus reglas de tarifa. Aquí nos concentramos en que la interfaz nunca mezcle una respuesta antigua con la selección actual.",{"type":426,"attrs":427,"content":429},"code_block",{"class":428},"language-typescript",[430],{"text":431,"type":133},"\u003Cscript setup lang=\"ts\">\nimport { onWatcherCleanup, ref, watch } from 'vue'\n\ntype Destination = '' | 'lima' | 'cusco'\ntype Quote = { amount: number; currency: 'PEN'; etaDays: number }\ntype Status = 'idle' | 'loading' | 'success' | 'error'\n\nconst destination = ref\u003CDestination>('')\nconst weightKg = ref(1)\nconst quote = ref\u003CQuote | null>(null)\nconst status = ref\u003CStatus>('idle')\nconst money = new Intl.NumberFormat('es-PE', {\n  style: 'currency',\n  currency: 'PEN',\n})\n\nwatch([destination, weightKg], ([zone, kg]) => {\n  // Nunca dejamos visible la tarifa del destino anterior.\n  quote.value = null\n\n  if (!zone || !Number.isFinite(kg) || kg \u003C= 0) {\n    status.value = 'idle'\n    return\n  }\n\n  status.value = 'loading'\n  const controller = new AbortController()\n  let current = true\n\n  // Agrupa cambios rápidos, por ejemplo al editar el peso.\n  const timer = setTimeout(async () => {\n    try {\n      const query = new URLSearchParams({\n        destination: zone,\n        weightKg: String(kg),\n      })\n      const response = await fetch(`/api/shipping/quote?${query}`, {\n        signal: controller.signal,\n      })\n\n      if (!response.ok) throw new Error(`HTTP ${response.status}`)\n      const result = (await response.json()) as Quote\n\n      // Protege también frente a clientes que no respeten la cancelación.\n      if (!current) return\n      quote.value = result\n      status.value = 'success'\n    } catch {\n      // Una cancelación esperada no es un error para el usuario.\n      if (!current || controller.signal.aborted) return\n      status.value = 'error'\n    }\n  }, 250)\n\n  // Debe registrarse durante la ejecución sincrónica del watcher.\n  onWatcherCleanup(() => {\n    current = false\n    clearTimeout(timer)\n    controller.abort()\n  })\n})\n\u003C/script>\n\n\u003Ctemplate>\n  \u003Csection aria-labelledby=\"shipping-title\">\n    \u003Ch2 id=\"shipping-title\">Cotiza tu envío\u003C/h2>\n\n    \u003Clabel for=\"destination\">Destino\u003C/label>\n    \u003Cselect id=\"destination\" v-model=\"destination\">\n      \u003Coption value=\"\">Selecciona un destino\u003C/option>\n      \u003Coption value=\"cusco\">Cusco\u003C/option>\n      \u003Coption value=\"lima\">Lima\u003C/option>\n    \u003C/select>\n\n    \u003Clabel for=\"weight\">Peso del paquete (kg)\u003C/label>\n    \u003Cinput id=\"weight\" v-model.number=\"weightKg\" type=\"number\" min=\"0.1\" step=\"0.1\" />\n\n    \u003Cp v-if=\"status === 'idle'\">Selecciona un destino y un peso válido.\u003C/p>\n    \u003Cp v-else-if=\"status === 'loading'\" role=\"status\">Calculando tarifa…\u003C/p>\n    \u003Cp v-else-if=\"status === 'error'\" role=\"alert\">\n      No pudimos calcular el envío. Cambia un dato para intentarlo de nuevo.\n    \u003C/p>\n    \u003Cp v-else-if=\"quote\" role=\"status\">\n      Envío: {{ money.format(quote.amount) }} · Entrega estimada: {{ quote.etaDays }} día(s)\n    \u003C/p>\n  \u003C/section>\n\u003C/template>\n",{"type":128,"attrs":433,"content":434},{"textAlign":125},[435],{"text":436,"type":133},"El orden dentro del watcher importa. Primero borramos la tarifa vieja. Después validamos los datos. Solo entonces iniciamos el temporizador y registramos la limpieza. Cuando cambia el destino o el peso, Vue ejecuta esa limpieza antes de volver a correr el watcher: cancela el temporizador si aún no se disparó, aborta la petición si ya empezó y marca su respuesta como inválida.",{"type":128,"attrs":438,"content":439},{"textAlign":125},[440,442,446,448,451,453,457,459,463],{"text":441,"type":133},"El booleano ",{"text":443,"type":133,"marks":444},"current",[445],{"type":155},{"text":447,"type":133}," parece redundante junto a ",{"text":159,"type":133,"marks":449},[450],{"type":155},{"text":452,"type":133},", pero protege el momento entre recibir una respuesta y actualizar la interfaz, y sirve si sustituyes ",{"text":454,"type":133,"marks":455},"fetch",[456],{"type":155},{"text":458,"type":133}," por una biblioteca que no propaga bien la cancelación. En el ",{"text":460,"type":133,"marks":461},"catch",[462],{"type":155},{"text":464,"type":133}," ignoramos las cancelaciones; los errores reales sí muestran un estado visible.",{"type":177,"attrs":466,"content":467},{"level":179,"textAlign":125},[468],{"text":469,"type":133},"Un endpoint de demostración para reproducir la carrera",{"type":128,"attrs":471,"content":472},{"textAlign":125},[473,475,479,481,485],{"text":474,"type":133},"Si usas Nuxt, crea ",{"text":476,"type":133,"marks":477},"server/api/shipping/quote.get.ts",[478],{"type":155},{"text":480,"type":133},". Las tarifas y las demoras son ",{"text":482,"type":133,"marks":483},"simuladas",[484],{"type":138},{"text":486,"type":133},": permiten comprobar el comportamiento sin contratar un servicio de envíos. Sustituye esta lógica por tu proveedor o reglas de negocio cuando integres el checkout.",{"type":426,"attrs":488,"content":489},{"class":428},[490],{"text":491,"type":133},"export default defineEventHandler(async (event) => {\n  const query = getQuery(event)\n  const destination = String(query.destination ?? '')\n  const weightKg = Number(query.weightKg)\n\n  if (\n    !['lima', 'cusco'].includes(destination) ||\n    !Number.isFinite(weightKg) ||\n    weightKg \u003C= 0\n  ) {\n    throw createError({ statusCode: 400, statusMessage: 'Datos de envío inválidos' })\n  }\n\n  // Cusco tarda más a propósito para exponer las respuestas fuera de orden.\n  await new Promise((resolve) => setTimeout(resolve, destination === 'cusco' ? 900 : 120))\n\n  const base = destination === 'cusco' ? 18 : 10\n  return {\n    amount: Math.round((base + weightKg * 2) * 100) / 100,\n    currency: 'PEN',\n    etaDays: destination === 'cusco' ? 3 : 1,\n  }\n})\n",{"type":128,"attrs":493,"content":494},{"textAlign":125},[495,497,501,503,508],{"text":496,"type":133},"Para verificarlo, elige Cusco, espera alrededor de 300 ms para que empiece la petición y cambia a Lima. Con 1 kg, el resultado final debe ser ",{"text":498,"type":133,"marks":499},"S/ 12.00 y 1 día",[500],{"type":138},{"text":502,"type":133},", incluso después de que habría terminado la solicitud lenta de Cusco. Repite cambiando el peso varias veces: solo la última combinación debe producir una tarifa visible. En la pestaña ",{"text":504,"type":133,"marks":505},"Network",[506],{"type":507},"italic",{"text":509,"type":133}," del navegador podrás ver las peticiones anteriores canceladas.",{"type":177,"attrs":511,"content":512},{"level":179,"textAlign":125},[513],{"text":514,"type":133},"Dos límites que importan en producción",{"type":128,"attrs":516,"content":517},{"textAlign":125},[518,522],{"text":519,"type":133,"marks":520},"Cancelar en el navegador no garantiza que el servidor haya detenido su trabajo.",[521],{"type":138},{"text":523,"type":133}," El objetivo principal de este patrón es mantener correcta la interfaz y evitar trabajo innecesario cuando sea posible. El backend debe validar los parámetros y calcular de nuevo la tarifa al confirmar la compra; nunca debe confiar en el importe mostrado por el cliente.",{"type":128,"attrs":525,"content":526},{"textAlign":125},[527,530,534,537,540,542,546,548,551,553,556],{"text":165,"type":133,"marks":528},[529],{"type":155},{"text":531,"type":133,"marks":532}," requiere Vue 3.5+ y debe registrarse antes de cualquier ",[533],{"type":138},{"text":395,"type":133,"marks":535},[536],{"type":155},{"text":230,"type":133,"marks":538},[539],{"type":138},{"text":541,"type":133}," Si tu proyecto usa una versión anterior, recibe ",{"text":543,"type":133,"marks":544},"onCleanup",[545],{"type":155},{"text":547,"type":133}," como tercer argumento de ",{"text":13,"type":133,"marks":549},[550],{"type":155},{"text":552,"type":133}," y registra allí la misma función. Ese argumento también evita la restricción de registro sincrónico de ",{"text":165,"type":133,"marks":554},[555],{"type":155},{"text":230,"type":133},{"type":128,"attrs":558,"content":559},{"textAlign":125},[560],{"text":561,"type":133},"Este patrón sirve además para disponibilidad de inventario por almacén, precios según moneda o fechas y estimaciones de entrega. La idea común es sencilla: cuando cambian los datos de entrada, la respuesta anterior pierde el derecho de actualizar la pantalla.",{"type":177,"attrs":563,"content":564},{"level":179,"textAlign":125},[565],{"text":566,"type":133},"Lecturas recomendadas",{"type":568,"content":569},"bullet_list",[570,590,603,616],{"type":194,"content":571},[572],{"type":128,"attrs":573,"content":574},{"textAlign":125},[575,583,585,588],{"text":576,"type":133,"marks":577},"Watchers en la documentación oficial de Vue",[578],{"type":579,"attrs":580},"link",{"href":581,"uuid":125,"anchor":125,"target":125,"linktype":582},"https://vuejs.org/guide/essentials/watchers","url",{"text":584,"type":133},": fuentes, ",{"text":266,"type":133,"marks":586},[587],{"type":155},{"text":589,"type":133}," y limpieza de efectos.",{"type":194,"content":591},[592],{"type":128,"attrs":593,"content":594},{"textAlign":125},[595,601],{"text":596,"type":133,"marks":597},"Vue’s watch vs watchEffect, which should I use?",[598],{"type":579,"attrs":599},{"href":600,"uuid":125,"anchor":125,"target":125,"linktype":582},"https://www.vuemastery.com/blog/vues-watch-vs-watcheffect-which-should-i-use/",{"text":602,"type":133},", de Vue Mastery: comparación centrada en el control de dependencias.",{"type":194,"content":604},[605],{"type":128,"attrs":606,"content":607},{"textAlign":125},[608,614],{"text":609,"type":133,"marks":610},"Vue computed vs watch vs watchEffect: When to Use Each One",[611],{"type":579,"attrs":612},{"href":613,"uuid":125,"anchor":125,"target":125,"linktype":582},"https://todovue.blog/blog/vue-computed-watch-watcheffect-guide.en/",{"text":615,"type":133},", de TODOvue: guía para elegir entre valores derivados y efectos.",{"type":194,"content":617},[618],{"type":128,"attrs":619,"content":620},{"textAlign":125},[621,627],{"text":622,"type":133,"marks":623},"AbortController.abort() en MDN",[624],{"type":579,"attrs":625},{"href":626,"uuid":125,"anchor":125,"target":125,"linktype":582},"https://developer.mozilla.org/en-US/docs/Web/API/AbortController/abort",{"text":628,"type":133},": comportamiento de la cancelación en la plataforma web.",{"type":128,"attrs":630,"content":631},{"textAlign":125},[632,634,639],{"text":633,"type":133},"Si estás empezando con la Composition API, puedes leer primero ",{"text":77,"type":133,"marks":635},[636],{"type":579,"attrs":637},{"href":638,"uuid":125,"anchor":125,"target":125,"linktype":582},"https://elrincondevue.com/article-list/what-is-a-composable",{"text":640,"type":133}," en El Rincón de Vue.","posts","posts/watch-vs-watcheffect-cotizador-envios-vue-3",-100,[10,11,12,13,14],20030325,"ac7d4efd-df46-4487-8c75-8ccc9a79b2e4","2026-09-27T18:12:58.826Z","default","article-list/watch-vs-watcheffect-cotizador-envios-vue-3",[],1790869932697]