[{"data":1,"prerenderedAt":1035},["ShallowReactive",2],{"blog-\u002Fblog\u002Fpokemon-shopping-cart-vue3":3,"blog-surround-\u002Fblog\u002Fpokemon-shopping-cart-vue3":1029},{"id":4,"title":5,"body":6,"date":991,"description":992,"extension":993,"faq":994,"image":1010,"imageAlt":1011,"links":1012,"meta":1018,"navigation":168,"path":1019,"rawbody":1020,"readingTime":206,"seo":1021,"sitemap":1022,"stem":1023,"tags":1024,"tldr":1027,"updated":1010,"__hash__":1028},"blog\u002Fblog\u002Fpokemon-shopping-cart-vue3.md","Pokémon shopping cart with Vue 3, Axios and the PokéAPI",{"type":7,"value":8,"toc":983},"minimark",[9,14,43,46,97,111,115,127,134,364,370,374,381,660,669,673,684,911,918,922,929,933,979],[10,11,13],"h2",{"id":12},"what-does-the-demo-do","What does the demo do?",[15,16,17,18,22,23,26,27,30,31,34,35,42],"p",{},"This is a shopping cart demo that uses Pokémon as products. It's built with ",[19,20,21],"strong",{},"Vue 3",", the ",[19,24,25],{},"Composition API",", ",[19,28,29],{},"Axios"," and ",[19,32,33],{},"Tailwind CSS",", and it gets its catalog from the public ",[36,37,41],"a",{"href":38,"rel":39},"https:\u002F\u002Fpokeapi.co",[40],"nofollow","PokéAPI"," REST API.",[15,44,45],{},"Features:",[47,48,49,53,62,69,75,81,87],"ul",{},[50,51,52],"li",{},"A product catalog fetched dynamically from a REST API.",[50,54,55,30,58,61],{},[19,56,57],{},"Lazy loading",[19,59,60],{},"client-side pagination",": products load in batches of 10, up to 100.",[50,63,64,65,68],{},"A ",[19,66,67],{},"reactive cart"," that stays in sync with the product list.",[50,70,71,74],{},[19,72,73],{},"Quantity management",": add, increase, decrease and remove items.",[50,76,77,80],{},[19,78,79],{},"Computed totals"," for item count and price.",[50,82,83,86],{},[19,84,85],{},"Loading and error states"," for every request.",[50,88,64,89,92,93,96],{},[19,90,91],{},"responsive"," layout and a ",[19,94,95],{},"simulated checkout"," flow.",[98,99,102],"project-embed",{"src":100,"title":101},"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fembed\u002FNPKjGQX?default-tab=js%2Cresult&theme-id=dark","Pokémon shopping cart demo built with Vue 3",[15,103,104,105,110],{},"The ",[36,106,109],{"href":107,"rel":108},"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FNPKjGQX",[40],"live demo on CodePen",". Switch to the JS tab to read the full source.",[10,112,114],{"id":113},"how-does-it-fetch-products-from-the-pokéapi","How does it fetch products from the PokéAPI?",[15,116,117,118,122,123,126],{},"The PokéAPI list endpoint (",[119,120,121],"code",{},"\u002Fapi\u002Fv2\u002Fpokemon?limit=10&offset=0",") only returns each Pokémon's ",[19,124,125],{},"name and URL",". The image, types and stats need a separate request per Pokémon.",[15,128,129,130,133],{},"Making those requests one after another would be slow, so the app runs them in parallel with ",[119,131,132],{},"Promise.all",". A whole batch then resolves in about the time of its slowest request:",[135,136,141],"pre",{"className":137,"code":138,"language":139,"meta":140,"style":140},"language-js shiki shiki-themes github-dark","import axios from 'axios'\n\nconst BATCH_SIZE = 10\nconst MAX_PRODUCTS = 100\n\nconst fetchBatch = async (offset) => {\n  const { data } = await axios.get('https:\u002F\u002Fpokeapi.co\u002Fapi\u002Fv2\u002Fpokemon', {\n    params: { limit: BATCH_SIZE, offset },\n  })\n\n  return Promise.all(\n    data.results.map(({ url }) => axios.get(url).then(res => res.data))\n  )\n}\n","js","",[119,142,143,163,170,186,199,204,234,270,282,288,293,311,352,358],{"__ignoreMap":140},[144,145,148,152,156,159],"span",{"class":146,"line":147},"line",1,[144,149,151],{"class":150},"snl16","import",[144,153,155],{"class":154},"s95oV"," axios ",[144,157,158],{"class":150},"from",[144,160,162],{"class":161},"sU2Wk"," 'axios'\n",[144,164,166],{"class":146,"line":165},2,[144,167,169],{"emptyLinePlaceholder":168},true,"\n",[144,171,173,176,180,183],{"class":146,"line":172},3,[144,174,175],{"class":150},"const",[144,177,179],{"class":178},"sDLfK"," BATCH_SIZE",[144,181,182],{"class":150}," =",[144,184,185],{"class":178}," 10\n",[144,187,189,191,194,196],{"class":146,"line":188},4,[144,190,175],{"class":150},[144,192,193],{"class":178}," MAX_PRODUCTS",[144,195,182],{"class":150},[144,197,198],{"class":178}," 100\n",[144,200,202],{"class":146,"line":201},5,[144,203,169],{"emptyLinePlaceholder":168},[144,205,207,209,213,215,218,221,225,228,231],{"class":146,"line":206},6,[144,208,175],{"class":150},[144,210,212],{"class":211},"svObZ"," fetchBatch",[144,214,182],{"class":150},[144,216,217],{"class":150}," async",[144,219,220],{"class":154}," (",[144,222,224],{"class":223},"s9osk","offset",[144,226,227],{"class":154},") ",[144,229,230],{"class":150},"=>",[144,232,233],{"class":154}," {\n",[144,235,237,240,243,246,249,252,255,258,261,264,267],{"class":146,"line":236},7,[144,238,239],{"class":150},"  const",[144,241,242],{"class":154}," { ",[144,244,245],{"class":178},"data",[144,247,248],{"class":154}," } ",[144,250,251],{"class":150},"=",[144,253,254],{"class":150}," await",[144,256,257],{"class":154}," axios.",[144,259,260],{"class":211},"get",[144,262,263],{"class":154},"(",[144,265,266],{"class":161},"'https:\u002F\u002Fpokeapi.co\u002Fapi\u002Fv2\u002Fpokemon'",[144,268,269],{"class":154},", {\n",[144,271,273,276,279],{"class":146,"line":272},8,[144,274,275],{"class":154},"    params: { limit: ",[144,277,278],{"class":178},"BATCH_SIZE",[144,280,281],{"class":154},", offset },\n",[144,283,285],{"class":146,"line":284},9,[144,286,287],{"class":154},"  })\n",[144,289,291],{"class":146,"line":290},10,[144,292,169],{"emptyLinePlaceholder":168},[144,294,296,299,302,305,308],{"class":146,"line":295},11,[144,297,298],{"class":150},"  return",[144,300,301],{"class":178}," Promise",[144,303,304],{"class":154},".",[144,306,307],{"class":211},"all",[144,309,310],{"class":154},"(\n",[144,312,314,317,320,323,326,329,331,333,335,338,341,343,346,349],{"class":146,"line":313},12,[144,315,316],{"class":154},"    data.results.",[144,318,319],{"class":211},"map",[144,321,322],{"class":154},"(({ ",[144,324,325],{"class":223},"url",[144,327,328],{"class":154}," }) ",[144,330,230],{"class":150},[144,332,257],{"class":154},[144,334,260],{"class":211},[144,336,337],{"class":154},"(url).",[144,339,340],{"class":211},"then",[144,342,263],{"class":154},[144,344,345],{"class":223},"res",[144,347,348],{"class":150}," =>",[144,350,351],{"class":154}," res.data))\n",[144,353,355],{"class":146,"line":354},13,[144,356,357],{"class":154},"  )\n",[144,359,361],{"class":146,"line":360},14,[144,362,363],{"class":154},"}\n",[15,365,366],{},[367,368,369],"em",{},"Simplified for this article. See the CodePen for the exact implementation.",[10,371,373],{"id":372},"how-do-lazy-loading-and-pagination-work","How do lazy loading and pagination work?",[15,375,376,377,380],{},"The app doesn't request all 100 products up front. It loads ",[19,378,379],{},"10 at a time"," and fetches the next batch as the user pages through the catalog. This keeps the first load fast and avoids sending traffic to a free public API for data nobody looks at.",[135,382,384],{"className":137,"code":383,"language":139,"meta":140,"style":140},"import { ref, computed } from 'vue'\n\nconst products = ref([])\nconst loading = ref(false)\nconst error = ref(null)\n\nconst canLoadMore = computed(() => products.value.length \u003C MAX_PRODUCTS)\n\nconst loadMore = async () => {\n  if (loading.value || !canLoadMore.value) return\n  loading.value = true\n  error.value = null\n\n  try {\n    const batch = await fetchBatch(products.value.length)\n    products.value.push(...batch.map(toProduct))\n  } catch (e) {\n    error.value = 'Could not load products. Please try again.'\n  } finally {\n    loading.value = false\n  }\n}\n",[119,385,386,398,402,417,436,454,458,488,492,510,530,540,550,554,561,583,605,617,628,638,649,655],{"__ignoreMap":140},[144,387,388,390,393,395],{"class":146,"line":147},[144,389,151],{"class":150},[144,391,392],{"class":154}," { ref, computed } ",[144,394,158],{"class":150},[144,396,397],{"class":161}," 'vue'\n",[144,399,400],{"class":146,"line":165},[144,401,169],{"emptyLinePlaceholder":168},[144,403,404,406,409,411,414],{"class":146,"line":172},[144,405,175],{"class":150},[144,407,408],{"class":178}," products",[144,410,182],{"class":150},[144,412,413],{"class":211}," ref",[144,415,416],{"class":154},"([])\n",[144,418,419,421,424,426,428,430,433],{"class":146,"line":188},[144,420,175],{"class":150},[144,422,423],{"class":178}," loading",[144,425,182],{"class":150},[144,427,413],{"class":211},[144,429,263],{"class":154},[144,431,432],{"class":178},"false",[144,434,435],{"class":154},")\n",[144,437,438,440,443,445,447,449,452],{"class":146,"line":201},[144,439,175],{"class":150},[144,441,442],{"class":178}," error",[144,444,182],{"class":150},[144,446,413],{"class":211},[144,448,263],{"class":154},[144,450,451],{"class":178},"null",[144,453,435],{"class":154},[144,455,456],{"class":146,"line":206},[144,457,169],{"emptyLinePlaceholder":168},[144,459,460,462,465,467,470,473,475,478,481,484,486],{"class":146,"line":236},[144,461,175],{"class":150},[144,463,464],{"class":178}," canLoadMore",[144,466,182],{"class":150},[144,468,469],{"class":211}," computed",[144,471,472],{"class":154},"(() ",[144,474,230],{"class":150},[144,476,477],{"class":154}," products.value.",[144,479,480],{"class":178},"length",[144,482,483],{"class":150}," \u003C",[144,485,193],{"class":178},[144,487,435],{"class":154},[144,489,490],{"class":146,"line":272},[144,491,169],{"emptyLinePlaceholder":168},[144,493,494,496,499,501,503,506,508],{"class":146,"line":284},[144,495,175],{"class":150},[144,497,498],{"class":211}," loadMore",[144,500,182],{"class":150},[144,502,217],{"class":150},[144,504,505],{"class":154}," () ",[144,507,230],{"class":150},[144,509,233],{"class":154},[144,511,512,515,518,521,524,527],{"class":146,"line":290},[144,513,514],{"class":150},"  if",[144,516,517],{"class":154}," (loading.value ",[144,519,520],{"class":150},"||",[144,522,523],{"class":150}," !",[144,525,526],{"class":154},"canLoadMore.value) ",[144,528,529],{"class":150},"return\n",[144,531,532,535,537],{"class":146,"line":295},[144,533,534],{"class":154},"  loading.value ",[144,536,251],{"class":150},[144,538,539],{"class":178}," true\n",[144,541,542,545,547],{"class":146,"line":313},[144,543,544],{"class":154},"  error.value ",[144,546,251],{"class":150},[144,548,549],{"class":178}," null\n",[144,551,552],{"class":146,"line":354},[144,553,169],{"emptyLinePlaceholder":168},[144,555,556,559],{"class":146,"line":360},[144,557,558],{"class":150},"  try",[144,560,233],{"class":154},[144,562,564,567,570,572,574,576,579,581],{"class":146,"line":563},15,[144,565,566],{"class":150},"    const",[144,568,569],{"class":178}," batch",[144,571,182],{"class":150},[144,573,254],{"class":150},[144,575,212],{"class":211},[144,577,578],{"class":154},"(products.value.",[144,580,480],{"class":178},[144,582,435],{"class":154},[144,584,586,589,592,594,597,600,602],{"class":146,"line":585},16,[144,587,588],{"class":154},"    products.value.",[144,590,591],{"class":211},"push",[144,593,263],{"class":154},[144,595,596],{"class":150},"...",[144,598,599],{"class":154},"batch.",[144,601,319],{"class":211},[144,603,604],{"class":154},"(toProduct))\n",[144,606,608,611,614],{"class":146,"line":607},17,[144,609,610],{"class":154},"  } ",[144,612,613],{"class":150},"catch",[144,615,616],{"class":154}," (e) {\n",[144,618,620,623,625],{"class":146,"line":619},18,[144,621,622],{"class":154},"    error.value ",[144,624,251],{"class":150},[144,626,627],{"class":161}," 'Could not load products. Please try again.'\n",[144,629,631,633,636],{"class":146,"line":630},19,[144,632,610],{"class":154},[144,634,635],{"class":150},"finally",[144,637,233],{"class":154},[144,639,641,644,646],{"class":146,"line":640},20,[144,642,643],{"class":154},"    loading.value ",[144,645,251],{"class":150},[144,647,648],{"class":178}," false\n",[144,650,652],{"class":146,"line":651},21,[144,653,654],{"class":154},"  }\n",[144,656,658],{"class":146,"line":657},22,[144,659,363],{"class":154},[15,661,104,662,665,666,668],{},[119,663,664],{},"loading"," guard stops a second request for the same batch from starting while one is still in flight. Using ",[119,667,635],{}," means the loading state is always cleared, even when a request fails.",[10,670,672],{"id":671},"how-is-the-cart-state-managed","How is the cart state managed?",[15,674,675,676,679,680,683],{},"The raw API responses are transformed into a small ",[19,677,678],{},"product shape"," that the UI needs (id, name, image, price). The cart is a reactive list of products with quantities. Totals are ",[19,681,682],{},"computed",", never stored, so they can't fall out of sync:",[135,685,687],{"className":137,"code":686,"language":139,"meta":140,"style":140},"const cart = ref([])\n\nconst addToCart = (product) => {\n  const item = cart.value.find(item => item.id === product.id)\n  if (item) item.quantity++\n  else cart.value.push({ ...product, quantity: 1 })\n}\n\nconst totalItems = computed(() =>\n  cart.value.reduce((sum, item) => sum + item.quantity, 0)\n)\n\nconst totalPrice = computed(() =>\n  cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)\n)\n",[119,688,689,702,706,726,757,767,790,794,798,814,850,854,858,873,907],{"__ignoreMap":140},[144,690,691,693,696,698,700],{"class":146,"line":147},[144,692,175],{"class":150},[144,694,695],{"class":178}," cart",[144,697,182],{"class":150},[144,699,413],{"class":211},[144,701,416],{"class":154},[144,703,704],{"class":146,"line":165},[144,705,169],{"emptyLinePlaceholder":168},[144,707,708,710,713,715,717,720,722,724],{"class":146,"line":172},[144,709,175],{"class":150},[144,711,712],{"class":211}," addToCart",[144,714,182],{"class":150},[144,716,220],{"class":154},[144,718,719],{"class":223},"product",[144,721,227],{"class":154},[144,723,230],{"class":150},[144,725,233],{"class":154},[144,727,728,730,733,735,738,741,743,746,748,751,754],{"class":146,"line":188},[144,729,239],{"class":150},[144,731,732],{"class":178}," item",[144,734,182],{"class":150},[144,736,737],{"class":154}," cart.value.",[144,739,740],{"class":211},"find",[144,742,263],{"class":154},[144,744,745],{"class":223},"item",[144,747,348],{"class":150},[144,749,750],{"class":154}," item.id ",[144,752,753],{"class":150},"===",[144,755,756],{"class":154}," product.id)\n",[144,758,759,761,764],{"class":146,"line":201},[144,760,514],{"class":150},[144,762,763],{"class":154}," (item) item.quantity",[144,765,766],{"class":150},"++\n",[144,768,769,772,774,776,779,781,784,787],{"class":146,"line":206},[144,770,771],{"class":150},"  else",[144,773,737],{"class":154},[144,775,591],{"class":211},[144,777,778],{"class":154},"({ ",[144,780,596],{"class":150},[144,782,783],{"class":154},"product, quantity: ",[144,785,786],{"class":178},"1",[144,788,789],{"class":154}," })\n",[144,791,792],{"class":146,"line":236},[144,793,363],{"class":154},[144,795,796],{"class":146,"line":272},[144,797,169],{"emptyLinePlaceholder":168},[144,799,800,802,805,807,809,811],{"class":146,"line":284},[144,801,175],{"class":150},[144,803,804],{"class":178}," totalItems",[144,806,182],{"class":150},[144,808,469],{"class":211},[144,810,472],{"class":154},[144,812,813],{"class":150},"=>\n",[144,815,816,819,822,825,828,830,832,834,836,839,842,845,848],{"class":146,"line":290},[144,817,818],{"class":154},"  cart.value.",[144,820,821],{"class":211},"reduce",[144,823,824],{"class":154},"((",[144,826,827],{"class":223},"sum",[144,829,26],{"class":154},[144,831,745],{"class":223},[144,833,227],{"class":154},[144,835,230],{"class":150},[144,837,838],{"class":154}," sum ",[144,840,841],{"class":150},"+",[144,843,844],{"class":154}," item.quantity, ",[144,846,847],{"class":178},"0",[144,849,435],{"class":154},[144,851,852],{"class":146,"line":295},[144,853,435],{"class":154},[144,855,856],{"class":146,"line":313},[144,857,169],{"emptyLinePlaceholder":168},[144,859,860,862,865,867,869,871],{"class":146,"line":354},[144,861,175],{"class":150},[144,863,864],{"class":178}," totalPrice",[144,866,182],{"class":150},[144,868,469],{"class":211},[144,870,472],{"class":154},[144,872,813],{"class":150},[144,874,875,877,879,881,883,885,887,889,891,893,895,898,901,903,905],{"class":146,"line":360},[144,876,818],{"class":154},[144,878,821],{"class":211},[144,880,824],{"class":154},[144,882,827],{"class":223},[144,884,26],{"class":154},[144,886,745],{"class":223},[144,888,227],{"class":154},[144,890,230],{"class":150},[144,892,838],{"class":154},[144,894,841],{"class":150},[144,896,897],{"class":154}," item.price ",[144,899,900],{"class":150},"*",[144,902,844],{"class":154},[144,904,847],{"class":178},[144,906,435],{"class":154},[144,908,909],{"class":146,"line":563},[144,910,435],{"class":154},[15,912,913,914,917],{},"Because the catalog and the cart share the same product ",[119,915,916],{},"id",", the product list and the cart stay in sync.",[10,919,921],{"id":920},"what-happens-at-checkout","What happens at checkout?",[15,923,924,925,928],{},"The checkout is ",[19,926,927],{},"simulated",". It completes the shopping flow from browsing to ordering without a real payment provider, and no payment data is collected.",[10,930,932],{"id":931},"what-frontend-concepts-does-it-demonstrate","What frontend concepts does it demonstrate?",[47,934,935,941,950,956,967,973],{},[50,936,937,940],{},[19,938,939],{},"REST API integration"," with Axios and async\u002Fawait.",[50,942,943,946,947,949],{},[19,944,945],{},"Performance-conscious API use",": batching, parallel requests with ",[119,948,132],{},", lazy loading and a hard limit.",[50,951,952,955],{},[19,953,954],{},"Data transformation"," from API responses into a UI-friendly model.",[50,957,958,961,962,30,965,304],{},[19,959,960],{},"Reactive state management"," with ",[119,963,964],{},"ref",[119,966,682],{},[50,968,969,972],{},[19,970,971],{},"Resilient UX"," with loading and error states.",[50,974,975,978],{},[19,976,977],{},"Responsive UI"," with Tailwind CSS.",[980,981,982],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":140,"searchDepth":165,"depth":165,"links":984},[985,986,987,988,989,990],{"id":12,"depth":165,"text":13},{"id":113,"depth":165,"text":114},{"id":372,"depth":165,"text":373},{"id":671,"depth":165,"text":672},{"id":920,"depth":165,"text":921},{"id":931,"depth":165,"text":932},"2026-10-17","How a Vue 3 shopping cart demo consumes the PokéAPI REST API: batched lazy loading with Promise.all, client-side pagination, a reactive cart with computed totals, loading and error states, and a simulated checkout.","md",[995,998,1001,1004,1007],{"question":996,"answer":997},"Which technologies does the Pokémon shopping cart use?","Vue 3 with the Composition API, Axios for HTTP requests, Tailwind CSS for the responsive UI, and the public PokéAPI REST API as the product data source.",{"question":999,"answer":1000},"How are the Pokémon loaded?","The app loads Pokémon lazily in batches of 10, up to a maximum of 100. For each batch it fetches the list of Pokémon and then requests every Pokémon's details in parallel with Promise.all.",{"question":1002,"answer":1003},"Why use Promise.all to fetch Pokémon details?","The PokéAPI list endpoint only returns names and URLs, so each Pokémon's details need their own request. Promise.all sends those requests in parallel, so a batch takes about as long as its slowest request instead of the sum of all of them.",{"question":1005,"answer":1006},"How are cart totals calculated?","Cart items are reactive state, and the total quantity and total price are computed properties derived from them, so they update automatically when items are added, removed or change quantity.",{"question":1008,"answer":1009},"Does the checkout process real payments?","No. The checkout is simulated to show the full shopping flow. No payment data is collected or sent anywhere.",null,"A Pokémon shopping cart demo with product cards and a cart summary",[1013,1016],{"label":1014,"to":107,"icon":1015},"View on CodePen","pi pi-code",{"label":41,"to":38,"icon":1017},"pi pi-external-link",{},"\u002Fblog\u002Fpokemon-shopping-cart-vue3","---\ntitle: \"Pokémon shopping cart with Vue 3, Axios and the PokéAPI\"\ndescription: \"How a Vue 3 shopping cart demo consumes the PokéAPI REST API: batched lazy loading with Promise.all, client-side pagination, a reactive cart with computed totals, loading and error states, and a simulated checkout.\"\ndate: \"2026-10-17\"\ntags: [\"Vue 3\", \"Composition API\", \"Axios\", \"REST API\", \"Tailwind CSS\", \"CodePen\"]\ntldr: \"This demo turns the PokéAPI into a product catalog. Built with Vue 3, the Composition API, Axios and Tailwind CSS, it loads Pokémon in batches of 10 (up to 100) using Promise.all, keeps a reactive cart with computed quantities and totals, handles loading and error states, and ends with a simulated checkout.\"\nimageAlt: \"A Pokémon shopping cart demo with product cards and a cart summary\"\nreadingTime: 6\nlinks:\n  - label: \"View on CodePen\"\n    to: \"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FNPKjGQX\"\n    icon: \"pi pi-code\"\n  - label: \"PokéAPI\"\n    to: \"https:\u002F\u002Fpokeapi.co\"\n    icon: \"pi pi-external-link\"\nfaq:\n  - question: \"Which technologies does the Pokémon shopping cart use?\"\n    answer: \"Vue 3 with the Composition API, Axios for HTTP requests, Tailwind CSS for the responsive UI, and the public PokéAPI REST API as the product data source.\"\n  - question: \"How are the Pokémon loaded?\"\n    answer: \"The app loads Pokémon lazily in batches of 10, up to a maximum of 100. For each batch it fetches the list of Pokémon and then requests every Pokémon's details in parallel with Promise.all.\"\n  - question: \"Why use Promise.all to fetch Pokémon details?\"\n    answer: \"The PokéAPI list endpoint only returns names and URLs, so each Pokémon's details need their own request. Promise.all sends those requests in parallel, so a batch takes about as long as its slowest request instead of the sum of all of them.\"\n  - question: \"How are cart totals calculated?\"\n    answer: \"Cart items are reactive state, and the total quantity and total price are computed properties derived from them, so they update automatically when items are added, removed or change quantity.\"\n  - question: \"Does the checkout process real payments?\"\n    answer: \"No. The checkout is simulated to show the full shopping flow. No payment data is collected or sent anywhere.\"\n---\n\n## What does the demo do?\n\nThis is a shopping cart demo that uses Pokémon as products. It's built with **Vue 3**, the **Composition API**, **Axios** and **Tailwind CSS**, and it gets its catalog from the public [PokéAPI](https:\u002F\u002Fpokeapi.co) REST API.\n\nFeatures:\n\n- A product catalog fetched dynamically from a REST API.\n- **Lazy loading** and **client-side pagination**: products load in batches of 10, up to 100.\n- A **reactive cart** that stays in sync with the product list.\n- **Quantity management**: add, increase, decrease and remove items.\n- **Computed totals** for item count and price.\n- **Loading and error states** for every request.\n- A **responsive** layout and a **simulated checkout** flow.\n\n::project-embed{src=\"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fembed\u002FNPKjGQX?default-tab=js%2Cresult&theme-id=dark\" title=\"Pokémon shopping cart demo built with Vue 3\"}\nThe [live demo on CodePen](https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FNPKjGQX). Switch to the JS tab to read the full source.\n::\n\n## How does it fetch products from the PokéAPI?\n\nThe PokéAPI list endpoint (`\u002Fapi\u002Fv2\u002Fpokemon?limit=10&offset=0`) only returns each Pokémon's **name and URL**. The image, types and stats need a separate request per Pokémon.\n\nMaking those requests one after another would be slow, so the app runs them in parallel with `Promise.all`. A whole batch then resolves in about the time of its slowest request:\n\n```js\nimport axios from 'axios'\n\nconst BATCH_SIZE = 10\nconst MAX_PRODUCTS = 100\n\nconst fetchBatch = async (offset) => {\n  const { data } = await axios.get('https:\u002F\u002Fpokeapi.co\u002Fapi\u002Fv2\u002Fpokemon', {\n    params: { limit: BATCH_SIZE, offset },\n  })\n\n  return Promise.all(\n    data.results.map(({ url }) => axios.get(url).then(res => res.data))\n  )\n}\n```\n\n*Simplified for this article. See the CodePen for the exact implementation.*\n\n## How do lazy loading and pagination work?\n\nThe app doesn't request all 100 products up front. It loads **10 at a time** and fetches the next batch as the user pages through the catalog. This keeps the first load fast and avoids sending traffic to a free public API for data nobody looks at.\n\n```js\nimport { ref, computed } from 'vue'\n\nconst products = ref([])\nconst loading = ref(false)\nconst error = ref(null)\n\nconst canLoadMore = computed(() => products.value.length \u003C MAX_PRODUCTS)\n\nconst loadMore = async () => {\n  if (loading.value || !canLoadMore.value) return\n  loading.value = true\n  error.value = null\n\n  try {\n    const batch = await fetchBatch(products.value.length)\n    products.value.push(...batch.map(toProduct))\n  } catch (e) {\n    error.value = 'Could not load products. Please try again.'\n  } finally {\n    loading.value = false\n  }\n}\n```\n\nThe `loading` guard stops a second request for the same batch from starting while one is still in flight. Using `finally` means the loading state is always cleared, even when a request fails.\n\n## How is the cart state managed?\n\nThe raw API responses are transformed into a small **product shape** that the UI needs (id, name, image, price). The cart is a reactive list of products with quantities. Totals are **computed**, never stored, so they can't fall out of sync:\n\n```js\nconst cart = ref([])\n\nconst addToCart = (product) => {\n  const item = cart.value.find(item => item.id === product.id)\n  if (item) item.quantity++\n  else cart.value.push({ ...product, quantity: 1 })\n}\n\nconst totalItems = computed(() =>\n  cart.value.reduce((sum, item) => sum + item.quantity, 0)\n)\n\nconst totalPrice = computed(() =>\n  cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)\n)\n```\n\nBecause the catalog and the cart share the same product `id`, the product list and the cart stay in sync.\n\n## What happens at checkout?\n\nThe checkout is **simulated**. It completes the shopping flow from browsing to ordering without a real payment provider, and no payment data is collected.\n\n## What frontend concepts does it demonstrate?\n\n- **REST API integration** with Axios and async\u002Fawait.\n- **Performance-conscious API use**: batching, parallel requests with `Promise.all`, lazy loading and a hard limit.\n- **Data transformation** from API responses into a UI-friendly model.\n- **Reactive state management** with `ref` and `computed`.\n- **Resilient UX** with loading and error states.\n- **Responsive UI** with Tailwind CSS.\n",{"title":5,"description":992},{"loc":1019},"blog\u002Fpokemon-shopping-cart-vue3",[21,25,29,1025,33,1026],"REST API","CodePen","This demo turns the PokéAPI into a product catalog. Built with Vue 3, the Composition API, Axios and Tailwind CSS, it loads Pokémon in batches of 10 (up to 100) using Promise.all, keeps a reactive cart with computed quantities and totals, handles loading and error states, and ends with a simulated checkout.","qeTCmDfTTVSTuZwSpGZdnxgcIiNeBL69L8q7xkZjaHQ",[1010,1030],{"title":1031,"path":1032,"stem":1033,"description":1034,"children":-1},"Vue 3 Autocounter: an animated number counter component for Vue 3","\u002Fblog\u002Fvue3-autocounter","blog\u002Fvue3-autocounter","How vue3-autocounter works, how to install it, and how to use it to animate statistics, metrics and prices in Vue 3 and Nuxt apps, with every prop, event and method documented.",1792306174062]