[{"data":1,"prerenderedAt":823},["ShallowReactive",2],{"blog-\u002Fblog\u002Fvue3-todo-app-composition-api":3,"blog-surround-\u002Fblog\u002Fvue3-todo-app-composition-api":817},{"id":4,"title":5,"body":6,"date":783,"description":784,"extension":785,"faq":786,"image":799,"imageAlt":800,"links":801,"meta":805,"navigation":160,"path":806,"rawbody":807,"readingTime":205,"seo":808,"sitemap":809,"stem":810,"tags":811,"tldr":814,"updated":815,"__hash__":816},"blog\u002Fblog\u002Fvue3-todo-app-composition-api.md","Building a TODO app with the Vue 3 Composition API and localStorage",{"type":7,"value":8,"toc":775},"minimark",[9,14,27,75,83,99,103,126,328,333,336,340,347,363,542,546,563,706,712,716,719,723,726,771],[10,11,13],"h2",{"id":12},"what-does-the-app-do","What does the app do?",[15,16,17,18,22,23,26],"p",{},"It's a small task manager built with ",[19,20,21],"strong",{},"Vue 3"," and the ",[19,24,25],{},"Composition API",". You can:",[28,29,30,37,52,60,66],"ul",{},[31,32,33,36],"li",{},[19,34,35],{},"Create"," tasks.",[31,38,39,42,43,47,48,51],{},[19,40,41],{},"Complete"," a task, which moves it from ",[44,45,46],"em",{},"To-Do"," to ",[44,49,50],{},"Done",".",[31,53,54,57,58,51],{},[19,55,56],{},"Restore"," a completed task back to ",[44,59,46],{},[31,61,62,65],{},[19,63,64],{},"Delete"," a task.",[31,67,68,69,71,72,74],{},"Switch between ",[19,70,46],{}," and ",[19,73,50],{}," with a reactive tab interface.",[15,76,77,78,82],{},"Tasks are stored in the browser's ",[79,80,81],"code",{},"localStorage",", so they are still there after a reload. Destructive or status-changing actions show a confirmation first, so a misclick doesn't lose your work.",[84,85,88],"project-embed",{"src":86,"title":87},"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fembed\u002FPojRQOB?default-tab=js%2Cresult&theme-id=dark","Vue 3 TODO app built with the Composition API",[15,89,90,91,98],{},"The ",[92,93,97],"a",{"href":94,"rel":95},"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FPojRQOB",[96],"nofollow","live app on CodePen",". Switch to the JS tab to read the full source.",[10,100,102],{"id":101},"how-is-the-state-modeled","How is the state modeled?",[15,104,105,106,109,110,113,114,117,118,121,122,125],{},"All the state hangs off one reactive array. Each task is a plain object with a unique ",[79,107,108],{},"id",", a ",[79,111,112],{},"title"," and a ",[79,115,116],{},"done"," flag. The tab views are ",[19,119,120],{},"derived"," from that array with ",[79,123,124],{},"computed"," instead of being stored separately:",[127,128,133],"pre",{"className":129,"code":130,"language":131,"meta":132,"style":132},"language-js shiki shiki-themes github-dark","import { ref, computed } from 'vue'\n\nconst tasks = ref([])\nconst activeTab = ref('todo')\n\nconst todoTasks = computed(() => tasks.value.filter(task => !task.done))\nconst doneTasks = computed(() => tasks.value.filter(task => task.done))\n\nconst visibleTasks = computed(() =>\n  activeTab.value === 'todo' ? todoTasks.value : doneTasks.value\n)\n","js","",[79,134,135,155,162,182,203,208,248,277,282,299,323],{"__ignoreMap":132},[136,137,140,144,148,151],"span",{"class":138,"line":139},"line",1,[136,141,143],{"class":142},"snl16","import",[136,145,147],{"class":146},"s95oV"," { ref, computed } ",[136,149,150],{"class":142},"from",[136,152,154],{"class":153},"sU2Wk"," 'vue'\n",[136,156,158],{"class":138,"line":157},2,[136,159,161],{"emptyLinePlaceholder":160},true,"\n",[136,163,165,168,172,175,179],{"class":138,"line":164},3,[136,166,167],{"class":142},"const",[136,169,171],{"class":170},"sDLfK"," tasks",[136,173,174],{"class":142}," =",[136,176,178],{"class":177},"svObZ"," ref",[136,180,181],{"class":146},"([])\n",[136,183,185,187,190,192,194,197,200],{"class":138,"line":184},4,[136,186,167],{"class":142},[136,188,189],{"class":170}," activeTab",[136,191,174],{"class":142},[136,193,178],{"class":177},[136,195,196],{"class":146},"(",[136,198,199],{"class":153},"'todo'",[136,201,202],{"class":146},")\n",[136,204,206],{"class":138,"line":205},5,[136,207,161],{"emptyLinePlaceholder":160},[136,209,211,213,216,218,221,224,227,230,233,235,239,242,245],{"class":138,"line":210},6,[136,212,167],{"class":142},[136,214,215],{"class":170}," todoTasks",[136,217,174],{"class":142},[136,219,220],{"class":177}," computed",[136,222,223],{"class":146},"(() ",[136,225,226],{"class":142},"=>",[136,228,229],{"class":146}," tasks.value.",[136,231,232],{"class":177},"filter",[136,234,196],{"class":146},[136,236,238],{"class":237},"s9osk","task",[136,240,241],{"class":142}," =>",[136,243,244],{"class":142}," !",[136,246,247],{"class":146},"task.done))\n",[136,249,251,253,256,258,260,262,264,266,268,270,272,274],{"class":138,"line":250},7,[136,252,167],{"class":142},[136,254,255],{"class":170}," doneTasks",[136,257,174],{"class":142},[136,259,220],{"class":177},[136,261,223],{"class":146},[136,263,226],{"class":142},[136,265,229],{"class":146},[136,267,232],{"class":177},[136,269,196],{"class":146},[136,271,238],{"class":237},[136,273,241],{"class":142},[136,275,276],{"class":146}," task.done))\n",[136,278,280],{"class":138,"line":279},8,[136,281,161],{"emptyLinePlaceholder":160},[136,283,285,287,290,292,294,296],{"class":138,"line":284},9,[136,286,167],{"class":142},[136,288,289],{"class":170}," visibleTasks",[136,291,174],{"class":142},[136,293,220],{"class":177},[136,295,223],{"class":146},[136,297,298],{"class":142},"=>\n",[136,300,302,305,308,311,314,317,320],{"class":138,"line":301},10,[136,303,304],{"class":146},"  activeTab.value ",[136,306,307],{"class":142},"===",[136,309,310],{"class":153}," 'todo'",[136,312,313],{"class":142}," ?",[136,315,316],{"class":146}," todoTasks.value ",[136,318,319],{"class":142},":",[136,321,322],{"class":146}," doneTasks.value\n",[136,324,326],{"class":138,"line":325},11,[136,327,202],{"class":146},[15,329,330],{},[44,331,332],{},"Simplified for this article. See the CodePen for the exact implementation.",[15,334,335],{},"Keeping one source of truth and computing the views from it means the lists can't drift out of sync. Completing a task only flips one flag, and both tabs update automatically.",[10,337,339],{"id":338},"why-give-each-task-a-uuid","Why give each task a UUID?",[15,341,342,343,346],{},"Using the array index as an identifier breaks as soon as items are deleted or reordered. Giving each task a ",[19,344,345],{},"UUID"," when it is created provides:",[28,348,349,360],{},[31,350,351,352,355,356,359],{},"a stable ",[79,353,354],{},":key"," for ",[79,357,358],{},"v-for",", so Vue updates the DOM correctly,",[31,361,362],{},"a reliable way to find the task to complete, restore or delete.",[127,364,366],{"className":129,"code":365,"language":131,"meta":132,"style":132},"const addTask = (title) => {\n  tasks.value.push({ id: uuid(), title, done: false })\n}\n\nconst toggleTask = (id) => {\n  const task = tasks.value.find(task => task.id === id)\n  if (task) task.done = !task.done\n}\n\nconst removeTask = (id) => {\n  tasks.value = tasks.value.filter(task => task.id !== id)\n}\n",[79,367,368,390,413,418,422,441,470,486,490,494,513,537],{"__ignoreMap":132},[136,369,370,372,375,377,380,382,385,387],{"class":138,"line":139},[136,371,167],{"class":142},[136,373,374],{"class":177}," addTask",[136,376,174],{"class":142},[136,378,379],{"class":146}," (",[136,381,112],{"class":237},[136,383,384],{"class":146},") ",[136,386,226],{"class":142},[136,388,389],{"class":146}," {\n",[136,391,392,395,398,401,404,407,410],{"class":138,"line":157},[136,393,394],{"class":146},"  tasks.value.",[136,396,397],{"class":177},"push",[136,399,400],{"class":146},"({ id: ",[136,402,403],{"class":177},"uuid",[136,405,406],{"class":146},"(), title, done: ",[136,408,409],{"class":170},"false",[136,411,412],{"class":146}," })\n",[136,414,415],{"class":138,"line":164},[136,416,417],{"class":146},"}\n",[136,419,420],{"class":138,"line":184},[136,421,161],{"emptyLinePlaceholder":160},[136,423,424,426,429,431,433,435,437,439],{"class":138,"line":205},[136,425,167],{"class":142},[136,427,428],{"class":177}," toggleTask",[136,430,174],{"class":142},[136,432,379],{"class":146},[136,434,108],{"class":237},[136,436,384],{"class":146},[136,438,226],{"class":142},[136,440,389],{"class":146},[136,442,443,446,449,451,453,456,458,460,462,465,467],{"class":138,"line":210},[136,444,445],{"class":142},"  const",[136,447,448],{"class":170}," task",[136,450,174],{"class":142},[136,452,229],{"class":146},[136,454,455],{"class":177},"find",[136,457,196],{"class":146},[136,459,238],{"class":237},[136,461,241],{"class":142},[136,463,464],{"class":146}," task.id ",[136,466,307],{"class":142},[136,468,469],{"class":146}," id)\n",[136,471,472,475,478,481,483],{"class":138,"line":250},[136,473,474],{"class":142},"  if",[136,476,477],{"class":146}," (task) task.done ",[136,479,480],{"class":142},"=",[136,482,244],{"class":142},[136,484,485],{"class":146},"task.done\n",[136,487,488],{"class":138,"line":279},[136,489,417],{"class":146},[136,491,492],{"class":138,"line":284},[136,493,161],{"emptyLinePlaceholder":160},[136,495,496,498,501,503,505,507,509,511],{"class":138,"line":301},[136,497,167],{"class":142},[136,499,500],{"class":177}," removeTask",[136,502,174],{"class":142},[136,504,379],{"class":146},[136,506,108],{"class":237},[136,508,384],{"class":146},[136,510,226],{"class":142},[136,512,389],{"class":146},[136,514,515,518,520,522,524,526,528,530,532,535],{"class":138,"line":325},[136,516,517],{"class":146},"  tasks.value ",[136,519,480],{"class":142},[136,521,229],{"class":146},[136,523,232],{"class":177},[136,525,196],{"class":146},[136,527,238],{"class":237},[136,529,241],{"class":142},[136,531,464],{"class":146},[136,533,534],{"class":142},"!==",[136,536,469],{"class":146},[136,538,540],{"class":138,"line":539},12,[136,541,417],{"class":146},[10,543,545],{"id":544},"how-does-persistence-with-localstorage-work","How does persistence with localStorage work?",[15,547,548,550,551,554,555,558,559,562],{},[79,549,81],{}," only stores strings, so the task list is serialized with ",[79,552,553],{},"JSON.stringify"," when it is saved and parsed back with ",[79,556,557],{},"JSON.parse"," when it is loaded. Loading happens in ",[79,560,561],{},"onMounted",", once the app is running in the browser:",[127,564,566],{"className":129,"code":565,"language":131,"meta":132,"style":132},"import { onMounted } from 'vue'\n\nconst STORAGE_KEY = 'tasks'\n\nconst saveTasks = () => {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks.value))\n}\n\nonMounted(() => {\n  const saved = localStorage.getItem(STORAGE_KEY)\n  if (saved) tasks.value = JSON.parse(saved)\n})\n",[79,567,568,579,583,595,599,615,642,646,650,660,681,701],{"__ignoreMap":132},[136,569,570,572,575,577],{"class":138,"line":139},[136,571,143],{"class":142},[136,573,574],{"class":146}," { onMounted } ",[136,576,150],{"class":142},[136,578,154],{"class":153},[136,580,581],{"class":138,"line":157},[136,582,161],{"emptyLinePlaceholder":160},[136,584,585,587,590,592],{"class":138,"line":164},[136,586,167],{"class":142},[136,588,589],{"class":170}," STORAGE_KEY",[136,591,174],{"class":142},[136,593,594],{"class":153}," 'tasks'\n",[136,596,597],{"class":138,"line":184},[136,598,161],{"emptyLinePlaceholder":160},[136,600,601,603,606,608,611,613],{"class":138,"line":205},[136,602,167],{"class":142},[136,604,605],{"class":177}," saveTasks",[136,607,174],{"class":142},[136,609,610],{"class":146}," () ",[136,612,226],{"class":142},[136,614,389],{"class":146},[136,616,617,620,623,625,628,631,634,636,639],{"class":138,"line":210},[136,618,619],{"class":146},"  localStorage.",[136,621,622],{"class":177},"setItem",[136,624,196],{"class":146},[136,626,627],{"class":170},"STORAGE_KEY",[136,629,630],{"class":146},", ",[136,632,633],{"class":170},"JSON",[136,635,51],{"class":146},[136,637,638],{"class":177},"stringify",[136,640,641],{"class":146},"(tasks.value))\n",[136,643,644],{"class":138,"line":250},[136,645,417],{"class":146},[136,647,648],{"class":138,"line":279},[136,649,161],{"emptyLinePlaceholder":160},[136,651,652,654,656,658],{"class":138,"line":284},[136,653,561],{"class":177},[136,655,223],{"class":146},[136,657,226],{"class":142},[136,659,389],{"class":146},[136,661,662,664,667,669,672,675,677,679],{"class":138,"line":301},[136,663,445],{"class":142},[136,665,666],{"class":170}," saved",[136,668,174],{"class":142},[136,670,671],{"class":146}," localStorage.",[136,673,674],{"class":177},"getItem",[136,676,196],{"class":146},[136,678,627],{"class":170},[136,680,202],{"class":146},[136,682,683,685,688,690,693,695,698],{"class":138,"line":325},[136,684,474],{"class":142},[136,686,687],{"class":146}," (saved) tasks.value ",[136,689,480],{"class":142},[136,691,692],{"class":170}," JSON",[136,694,51],{"class":146},[136,696,697],{"class":177},"parse",[136,699,700],{"class":146},"(saved)\n",[136,702,703],{"class":138,"line":539},[136,704,705],{"class":146},"})\n",[15,707,708,709,711],{},"Saving after every change keeps ",[79,710,81],{}," and the UI in sync, with no backend at all.",[10,713,715],{"id":714},"why-confirm-destructive-actions","Why confirm destructive actions?",[15,717,718],{},"Deleting a task or changing its status are the two actions a user is most likely to regret. Asking for confirmation before running them is cheap and saves frustration, and it's a habit worth keeping in larger apps.",[10,720,722],{"id":721},"what-frontend-concepts-does-it-demonstrate","What frontend concepts does it demonstrate?",[15,724,725],{},"Small as it is, the project covers concepts that come up in any production Vue app:",[28,727,728,737,745,753,759,765],{},[31,729,730,733,734,51],{},[19,731,732],{},"Reactive state"," with ",[79,735,736],{},"ref",[31,738,739,733,742,744],{},[19,740,741],{},"Derived state",[79,743,124],{},", using one source of truth.",[31,746,747,379,750,752],{},[19,748,749],{},"Lifecycle hooks",[79,751,561],{},") for browser-only work.",[31,754,755,758],{},[19,756,757],{},"Client-side persistence"," with the Web Storage API.",[31,760,761,764],{},[19,762,763],{},"Stable identity"," for list items using UUIDs.",[31,766,767,770],{},[19,768,769],{},"Event-driven UI"," with confirmation flows.",[772,773,774],"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":132,"searchDepth":157,"depth":157,"links":776},[777,778,779,780,781,782],{"id":12,"depth":157,"text":13},{"id":101,"depth":157,"text":102},{"id":338,"depth":157,"text":339},{"id":544,"depth":157,"text":545},{"id":714,"depth":157,"text":715},{"id":721,"depth":157,"text":722},"2026-10-17","A walkthrough of a Vue 3 TODO app built with ref, computed and onMounted: reactive task state, To-Do and Done tabs, UUID task IDs, confirmation flows and localStorage persistence.","md",[787,790,793,796],{"question":788,"answer":789},"Which Vue 3 APIs does the TODO app use?","It uses ref for the reactive task list, computed for the derived To-Do and Done lists, and onMounted to load saved tasks from localStorage when the app starts.",{"question":791,"answer":792},"How does the app keep tasks after a page reload?","Every change to the task list is serialized with JSON.stringify and written to localStorage. On mount, the app reads that key back with JSON.parse and restores the tasks.",{"question":794,"answer":795},"Why use UUIDs for the tasks?","A UUID gives each task a stable, unique identifier that does not depend on its position in the array. That makes completing, restoring and deleting tasks reliable, and it gives Vue a correct key for v-for.",{"question":797,"answer":798},"Can I try the app?","Yes. The full source code and a live, editable version are on CodePen, embedded in this post.","\u002Fimages\u002Fprojects\u002Fvue3-todo-app-demo.png","The Vue 3 TODO app showing To-Do and Done tabs with a list of tasks",[802],{"label":803,"to":94,"icon":804},"View on CodePen","pi pi-code",{},"\u002Fblog\u002Fvue3-todo-app-composition-api","---\ntitle: \"Building a TODO app with the Vue 3 Composition API and localStorage\"\ndescription: \"A walkthrough of a Vue 3 TODO app built with ref, computed and onMounted: reactive task state, To-Do and Done tabs, UUID task IDs, confirmation flows and localStorage persistence.\"\ndate: \"2026-10-17\"\ntags: [\"Vue 3\", \"Composition API\", \"JavaScript\", \"localStorage\", \"CodePen\"]\ntldr: \"This is a small task manager built with Vue 3 and the Composition API. Tasks live in a reactive ref, the To-Do and Done lists are computed from it, every task gets a UUID, and the list is saved to localStorage so it survives page reloads. Destructive and status-changing actions ask for confirmation first.\"\nimage: \"\u002Fimages\u002Fprojects\u002Fvue3-todo-app-demo.png\"\nimageAlt: \"The Vue 3 TODO app showing To-Do and Done tabs with a list of tasks\"\nreadingTime: 5\nlinks:\n  - label: \"View on CodePen\"\n    to: \"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FPojRQOB\"\n    icon: \"pi pi-code\"\nfaq:\n  - question: \"Which Vue 3 APIs does the TODO app use?\"\n    answer: \"It uses ref for the reactive task list, computed for the derived To-Do and Done lists, and onMounted to load saved tasks from localStorage when the app starts.\"\n  - question: \"How does the app keep tasks after a page reload?\"\n    answer: \"Every change to the task list is serialized with JSON.stringify and written to localStorage. On mount, the app reads that key back with JSON.parse and restores the tasks.\"\n  - question: \"Why use UUIDs for the tasks?\"\n    answer: \"A UUID gives each task a stable, unique identifier that does not depend on its position in the array. That makes completing, restoring and deleting tasks reliable, and it gives Vue a correct key for v-for.\"\n  - question: \"Can I try the app?\"\n    answer: \"Yes. The full source code and a live, editable version are on CodePen, embedded in this post.\"\n---\n\n## What does the app do?\n\nIt's a small task manager built with **Vue 3** and the **Composition API**. You can:\n\n- **Create** tasks.\n- **Complete** a task, which moves it from *To-Do* to *Done*.\n- **Restore** a completed task back to *To-Do*.\n- **Delete** a task.\n- Switch between **To-Do** and **Done** with a reactive tab interface.\n\nTasks are stored in the browser's `localStorage`, so they are still there after a reload. Destructive or status-changing actions show a confirmation first, so a misclick doesn't lose your work.\n\n::project-embed{src=\"https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fembed\u002FPojRQOB?default-tab=js%2Cresult&theme-id=dark\" title=\"Vue 3 TODO app built with the Composition API\"}\nThe [live app on CodePen](https:\u002F\u002Fcodepen.io\u002Fps_cristopher\u002Fpen\u002FPojRQOB). Switch to the JS tab to read the full source.\n::\n\n## How is the state modeled?\n\nAll the state hangs off one reactive array. Each task is a plain object with a unique `id`, a `title` and a `done` flag. The tab views are **derived** from that array with `computed` instead of being stored separately:\n\n```js\nimport { ref, computed } from 'vue'\n\nconst tasks = ref([])\nconst activeTab = ref('todo')\n\nconst todoTasks = computed(() => tasks.value.filter(task => !task.done))\nconst doneTasks = computed(() => tasks.value.filter(task => task.done))\n\nconst visibleTasks = computed(() =>\n  activeTab.value === 'todo' ? todoTasks.value : doneTasks.value\n)\n```\n\n*Simplified for this article. See the CodePen for the exact implementation.*\n\nKeeping one source of truth and computing the views from it means the lists can't drift out of sync. Completing a task only flips one flag, and both tabs update automatically.\n\n## Why give each task a UUID?\n\nUsing the array index as an identifier breaks as soon as items are deleted or reordered. Giving each task a **UUID** when it is created provides:\n\n- a stable `:key` for `v-for`, so Vue updates the DOM correctly,\n- a reliable way to find the task to complete, restore or delete.\n\n```js\nconst addTask = (title) => {\n  tasks.value.push({ id: uuid(), title, done: false })\n}\n\nconst toggleTask = (id) => {\n  const task = tasks.value.find(task => task.id === id)\n  if (task) task.done = !task.done\n}\n\nconst removeTask = (id) => {\n  tasks.value = tasks.value.filter(task => task.id !== id)\n}\n```\n\n## How does persistence with localStorage work?\n\n`localStorage` only stores strings, so the task list is serialized with `JSON.stringify` when it is saved and parsed back with `JSON.parse` when it is loaded. Loading happens in `onMounted`, once the app is running in the browser:\n\n```js\nimport { onMounted } from 'vue'\n\nconst STORAGE_KEY = 'tasks'\n\nconst saveTasks = () => {\n  localStorage.setItem(STORAGE_KEY, JSON.stringify(tasks.value))\n}\n\nonMounted(() => {\n  const saved = localStorage.getItem(STORAGE_KEY)\n  if (saved) tasks.value = JSON.parse(saved)\n})\n```\n\nSaving after every change keeps `localStorage` and the UI in sync, with no backend at all.\n\n## Why confirm destructive actions?\n\nDeleting a task or changing its status are the two actions a user is most likely to regret. Asking for confirmation before running them is cheap and saves frustration, and it's a habit worth keeping in larger apps.\n\n## What frontend concepts does it demonstrate?\n\nSmall as it is, the project covers concepts that come up in any production Vue app:\n\n- **Reactive state** with `ref`.\n- **Derived state** with `computed`, using one source of truth.\n- **Lifecycle hooks** (`onMounted`) for browser-only work.\n- **Client-side persistence** with the Web Storage API.\n- **Stable identity** for list items using UUIDs.\n- **Event-driven UI** with confirmation flows.\n",{"title":5,"description":784},{"loc":806},"blog\u002Fvue3-todo-app-composition-api",[21,25,812,81,813],"JavaScript","CodePen","This is a small task manager built with Vue 3 and the Composition API. Tasks live in a reactive ref, the To-Do and Done lists are computed from it, every task gets a UUID, and the list is saved to localStorage so it survives page reloads. Destructive and status-changing actions ask for confirmation first.",null,"XZcXFWq2gp70yiRkknS_3ozvjvNKxu4h9nbBt3-eGxM",[818,815],{"title":819,"path":820,"stem":821,"description":822,"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]