[{"data":1,"prerenderedAt":2272},["ShallowReactive",2],{"post-en-vueuse-essentiels":3},{"id":4,"title":5,"body":6,"date":2258,"description":17,"excerpt":2259,"extension":2260,"meta":2261,"navigation":144,"path":2262,"readTime":201,"seo":2263,"slug":2264,"stem":2265,"tags":2266,"__hash__":2271},"en_blog/en/blog/vueuse-essentiels.md","VueUse: The Composables That Actually Make a Difference",{"type":7,"value":8,"toc":2236},"minimark",[9,14,18,23,49,52,59,62,289,295,446,454,492,501,512,515,655,660,729,732,742,844,851,854,979,996,1003,1006,1195,1198,1290,1296,1303,1449,1463,1470,1518,1577,1588,1595,1711,1714,1721,1724,1898,1901,1996,2002,2009,2015,2134,2202,2208,2212,2232],[10,11,13],"h1",{"id":12},"vueuse-in-production-the-composables-that-actually-make-a-difference","VueUse in Production: The Composables That Actually Make a Difference",[15,16,17],"p",{},"VueUse ships over 200 composables. The documentation lists all of them, which does not help you determine which ones are genuinely worth learning. Here are the ones that appear consistently on professional projects, along with the concrete situations where they save meaningful time.",[19,20,22],"h2",{"id":21},"installation","Installation",[24,25,30],"pre",{"className":26,"code":27,"language":28,"meta":29,"style":29},"language-bash shiki shiki-themes github-dark github-light","npm install @vueuse/core\n","bash","",[31,32,33],"code",{"__ignoreMap":29},[34,35,38,42,46],"span",{"class":36,"line":37},"line",1,[34,39,41],{"class":40},"s-Z4r","npm",[34,43,45],{"class":44},"sg6BJ"," install",[34,47,48],{"class":44}," @vueuse/core\n",[15,50,51],{},"VueUse is compatible with Vue 3 and Nuxt 3/4. All composables are tree-shakable — only those you import are included in the bundle.",[19,53,55,58],{"id":54},"useasyncstate-replacing-the-loadingerrordata-pattern",[31,56,57],{},"useAsyncState",": Replacing the loading/error/data Pattern",[15,60,61],{},"The most repetitive pattern in Vue.js:",[24,63,67],{"className":64,"code":65,"language":66,"meta":29,"style":29},"language-typescript shiki shiki-themes github-dark github-light","// What we write without VueUse — over and over\nconst data = ref(null)\nconst loading = ref(false)\nconst error = ref(null)\n\nconst fetch = async () => {\n  loading.value = true\n  error.value = null\n  try {\n    data.value = await api.getCertificates()\n  } catch (e) {\n    error.value = e\n  } finally {\n    loading.value = false\n  }\n}\n\nonMounted(fetch)\n","typescript",[31,68,69,75,102,121,139,146,168,180,191,199,219,231,242,252,263,269,275,280],{"__ignoreMap":29},[34,70,71],{"class":36,"line":37},[34,72,74],{"class":73},"sryI4","// What we write without VueUse — over and over\n",[34,76,78,82,86,89,92,96,99],{"class":36,"line":77},2,[34,79,81],{"class":80},"scx8i","const",[34,83,85],{"class":84},"s0DvM"," data",[34,87,88],{"class":80}," =",[34,90,91],{"class":40}," ref",[34,93,95],{"class":94},"sQ3_J","(",[34,97,98],{"class":84},"null",[34,100,101],{"class":94},")\n",[34,103,105,107,110,112,114,116,119],{"class":36,"line":104},3,[34,106,81],{"class":80},[34,108,109],{"class":84}," loading",[34,111,88],{"class":80},[34,113,91],{"class":40},[34,115,95],{"class":94},[34,117,118],{"class":84},"false",[34,120,101],{"class":94},[34,122,124,126,129,131,133,135,137],{"class":36,"line":123},4,[34,125,81],{"class":80},[34,127,128],{"class":84}," error",[34,130,88],{"class":80},[34,132,91],{"class":40},[34,134,95],{"class":94},[34,136,98],{"class":84},[34,138,101],{"class":94},[34,140,142],{"class":36,"line":141},5,[34,143,145],{"emptyLinePlaceholder":144},true,"\n",[34,147,149,151,154,156,159,162,165],{"class":36,"line":148},6,[34,150,81],{"class":80},[34,152,153],{"class":40}," fetch",[34,155,88],{"class":80},[34,157,158],{"class":80}," async",[34,160,161],{"class":94}," () ",[34,163,164],{"class":80},"=>",[34,166,167],{"class":94}," {\n",[34,169,171,174,177],{"class":36,"line":170},7,[34,172,173],{"class":94},"  loading.value ",[34,175,176],{"class":80},"=",[34,178,179],{"class":84}," true\n",[34,181,183,186,188],{"class":36,"line":182},8,[34,184,185],{"class":94},"  error.value ",[34,187,176],{"class":80},[34,189,190],{"class":84}," null\n",[34,192,194,197],{"class":36,"line":193},9,[34,195,196],{"class":80},"  try",[34,198,167],{"class":94},[34,200,202,205,207,210,213,216],{"class":36,"line":201},10,[34,203,204],{"class":94},"    data.value ",[34,206,176],{"class":80},[34,208,209],{"class":80}," await",[34,211,212],{"class":94}," api.",[34,214,215],{"class":40},"getCertificates",[34,217,218],{"class":94},"()\n",[34,220,222,225,228],{"class":36,"line":221},11,[34,223,224],{"class":94},"  } ",[34,226,227],{"class":80},"catch",[34,229,230],{"class":94}," (e) {\n",[34,232,234,237,239],{"class":36,"line":233},12,[34,235,236],{"class":94},"    error.value ",[34,238,176],{"class":80},[34,240,241],{"class":94}," e\n",[34,243,245,247,250],{"class":36,"line":244},13,[34,246,224],{"class":94},[34,248,249],{"class":80},"finally",[34,251,167],{"class":94},[34,253,255,258,260],{"class":36,"line":254},14,[34,256,257],{"class":94},"    loading.value ",[34,259,176],{"class":80},[34,261,262],{"class":84}," false\n",[34,264,266],{"class":36,"line":265},15,[34,267,268],{"class":94},"  }\n",[34,270,272],{"class":36,"line":271},16,[34,273,274],{"class":94},"}\n",[34,276,278],{"class":36,"line":277},17,[34,279,145],{"emptyLinePlaceholder":144},[34,281,283,286],{"class":36,"line":282},18,[34,284,285],{"class":40},"onMounted",[34,287,288],{"class":94},"(fetch)\n",[15,290,291,292,294],{},"With ",[31,293,57],{},":",[24,296,298],{"className":64,"code":297,"language":66,"meta":29,"style":29},"import { useAsyncState } from \"@vueuse/core\"\n\nconst { state, isLoading, error, execute } = useAsyncState(\n  () => api.getCertificates(),\n  [], // Initial value\n  {\n    immediate: true, // Execute on mount\n    resetOnExecute: true, // Reset to initial value before each execution\n    onError: (e) => logger.error(\"Fetch failed\", e),\n  },\n)\n",[31,299,300,314,318,355,369,377,382,395,407,437,442],{"__ignoreMap":29},[34,301,302,305,308,311],{"class":36,"line":37},[34,303,304],{"class":80},"import",[34,306,307],{"class":94}," { useAsyncState } ",[34,309,310],{"class":80},"from",[34,312,313],{"class":44}," \"@vueuse/core\"\n",[34,315,316],{"class":36,"line":77},[34,317,145],{"emptyLinePlaceholder":144},[34,319,320,322,325,328,331,334,336,339,341,344,347,349,352],{"class":36,"line":104},[34,321,81],{"class":80},[34,323,324],{"class":94}," { ",[34,326,327],{"class":84},"state",[34,329,330],{"class":94},", ",[34,332,333],{"class":84},"isLoading",[34,335,330],{"class":94},[34,337,338],{"class":84},"error",[34,340,330],{"class":94},[34,342,343],{"class":84},"execute",[34,345,346],{"class":94}," } ",[34,348,176],{"class":80},[34,350,351],{"class":40}," useAsyncState",[34,353,354],{"class":94},"(\n",[34,356,357,360,362,364,366],{"class":36,"line":123},[34,358,359],{"class":94},"  () ",[34,361,164],{"class":80},[34,363,212],{"class":94},[34,365,215],{"class":40},[34,367,368],{"class":94},"(),\n",[34,370,371,374],{"class":36,"line":141},[34,372,373],{"class":94},"  [], ",[34,375,376],{"class":73},"// Initial value\n",[34,378,379],{"class":36,"line":148},[34,380,381],{"class":94},"  {\n",[34,383,384,387,390,392],{"class":36,"line":170},[34,385,386],{"class":94},"    immediate: ",[34,388,389],{"class":84},"true",[34,391,330],{"class":94},[34,393,394],{"class":73},"// Execute on mount\n",[34,396,397,400,402,404],{"class":36,"line":182},[34,398,399],{"class":94},"    resetOnExecute: ",[34,401,389],{"class":84},[34,403,330],{"class":94},[34,405,406],{"class":73},"// Reset to initial value before each execution\n",[34,408,409,412,415,419,422,424,427,429,431,434],{"class":36,"line":193},[34,410,411],{"class":40},"    onError",[34,413,414],{"class":94},": (",[34,416,418],{"class":417},"sFbx2","e",[34,420,421],{"class":94},") ",[34,423,164],{"class":80},[34,425,426],{"class":94}," logger.",[34,428,338],{"class":40},[34,430,95],{"class":94},[34,432,433],{"class":44},"\"Fetch failed\"",[34,435,436],{"class":94},", e),\n",[34,438,439],{"class":36,"line":201},[34,440,441],{"class":94},"  },\n",[34,443,444],{"class":36,"line":221},[34,445,101],{"class":94},[15,447,448,450,451,453],{},[31,449,327],{}," is typed from the return type of the async function. ",[31,452,343],{}," allows re-triggering manually with different parameters:",[24,455,457],{"className":64,"code":456,"language":66,"meta":29,"style":29},"// Re-fetch with a different filter\nawait execute(0, { status: \"ACTIVE\", period: \"2024-01\" })\n",[31,458,459,464],{"__ignoreMap":29},[34,460,461],{"class":36,"line":37},[34,462,463],{"class":73},"// Re-fetch with a different filter\n",[34,465,466,469,472,474,477,480,483,486,489],{"class":36,"line":77},[34,467,468],{"class":80},"await",[34,470,471],{"class":40}," execute",[34,473,95],{"class":94},[34,475,476],{"class":84},"0",[34,478,479],{"class":94},", { status: ",[34,481,482],{"class":44},"\"ACTIVE\"",[34,484,485],{"class":94},", period: ",[34,487,488],{"class":44},"\"2024-01\"",[34,490,491],{"class":94}," })\n",[15,493,494,495,497,498,500],{},"The second argument to ",[31,496,343],{}," (the delay) is a legacy of the API — pass ",[31,499,476],{}," for immediate execution.",[19,502,504,507,508,511],{"id":503},"usedebouncefn-and-usethrottlefn-performance-on-frequent-events",[31,505,506],{},"useDebounceFn"," and ",[31,509,510],{},"useThrottleFn",": Performance on Frequent Events",[15,513,514],{},"On a search field that calls an API on every keystroke:",[24,516,518],{"className":64,"code":517,"language":66,"meta":29,"style":29},"import { useDebounceFn } from \"@vueuse/core\"\n\nconst search = ref(\"\")\n\nconst searchApi = useDebounceFn(async (query: string) => {\n  if (query.length \u003C 2) return\n  results.value = await api.search(query)\n}, 350) // 350ms after the last keystroke\n\nwatch(search, searchApi)\n",[31,519,520,531,535,553,557,591,613,630,643,647],{"__ignoreMap":29},[34,521,522,524,527,529],{"class":36,"line":37},[34,523,304],{"class":80},[34,525,526],{"class":94}," { useDebounceFn } ",[34,528,310],{"class":80},[34,530,313],{"class":44},[34,532,533],{"class":36,"line":77},[34,534,145],{"emptyLinePlaceholder":144},[34,536,537,539,542,544,546,548,551],{"class":36,"line":104},[34,538,81],{"class":80},[34,540,541],{"class":84}," search",[34,543,88],{"class":80},[34,545,91],{"class":40},[34,547,95],{"class":94},[34,549,550],{"class":44},"\"\"",[34,552,101],{"class":94},[34,554,555],{"class":36,"line":123},[34,556,145],{"emptyLinePlaceholder":144},[34,558,559,561,564,566,569,571,574,577,580,582,585,587,589],{"class":36,"line":141},[34,560,81],{"class":80},[34,562,563],{"class":84}," searchApi",[34,565,88],{"class":80},[34,567,568],{"class":40}," useDebounceFn",[34,570,95],{"class":94},[34,572,573],{"class":80},"async",[34,575,576],{"class":94}," (",[34,578,579],{"class":417},"query",[34,581,294],{"class":80},[34,583,584],{"class":84}," string",[34,586,421],{"class":94},[34,588,164],{"class":80},[34,590,167],{"class":94},[34,592,593,596,599,602,605,608,610],{"class":36,"line":148},[34,594,595],{"class":80},"  if",[34,597,598],{"class":94}," (query.",[34,600,601],{"class":84},"length",[34,603,604],{"class":80}," \u003C",[34,606,607],{"class":84}," 2",[34,609,421],{"class":94},[34,611,612],{"class":80},"return\n",[34,614,615,618,620,622,624,627],{"class":36,"line":170},[34,616,617],{"class":94},"  results.value ",[34,619,176],{"class":80},[34,621,209],{"class":80},[34,623,212],{"class":94},[34,625,626],{"class":40},"search",[34,628,629],{"class":94},"(query)\n",[34,631,632,635,638,640],{"class":36,"line":182},[34,633,634],{"class":94},"}, ",[34,636,637],{"class":84},"350",[34,639,421],{"class":94},[34,641,642],{"class":73},"// 350ms after the last keystroke\n",[34,644,645],{"class":36,"line":193},[34,646,145],{"emptyLinePlaceholder":144},[34,648,649,652],{"class":36,"line":201},[34,650,651],{"class":40},"watch",[34,653,654],{"class":94},"(search, searchApi)\n",[15,656,657,659],{},[31,658,510],{}," for cases where you want to guarantee at most one execution per interval (scroll, resize, mousemove):",[24,661,663],{"className":64,"code":662,"language":66,"meta":29,"style":29},"import { useThrottleFn } from \"@vueuse/core\"\n\nconst onScroll = useThrottleFn((event: Event) => {\n  updateScrollPosition(window.scrollY)\n}, 100) // At most one execution per 100ms\n",[31,664,665,676,680,709,717],{"__ignoreMap":29},[34,666,667,669,672,674],{"class":36,"line":37},[34,668,304],{"class":80},[34,670,671],{"class":94}," { useThrottleFn } ",[34,673,310],{"class":80},[34,675,313],{"class":44},[34,677,678],{"class":36,"line":77},[34,679,145],{"emptyLinePlaceholder":144},[34,681,682,684,687,689,692,695,698,700,703,705,707],{"class":36,"line":104},[34,683,81],{"class":80},[34,685,686],{"class":84}," onScroll",[34,688,88],{"class":80},[34,690,691],{"class":40}," useThrottleFn",[34,693,694],{"class":94},"((",[34,696,697],{"class":417},"event",[34,699,294],{"class":80},[34,701,702],{"class":40}," Event",[34,704,421],{"class":94},[34,706,164],{"class":80},[34,708,167],{"class":94},[34,710,711,714],{"class":36,"line":123},[34,712,713],{"class":40},"  updateScrollPosition",[34,715,716],{"class":94},"(window.scrollY)\n",[34,718,719,721,724,726],{"class":36,"line":141},[34,720,634],{"class":94},[34,722,723],{"class":84},"100",[34,725,421],{"class":94},[34,727,728],{"class":73},"// At most one execution per 100ms\n",[15,730,731],{},"The distinction: debounce waits for activity to stop, throttle executes at regular intervals during activity. The practical rule: debounce for search, throttle for scroll.",[19,733,735,507,738,741],{"id":734},"uselocalstorage-and-usesessionstorage-reactive-persistent-state",[31,736,737],{},"useLocalStorage",[31,739,740],{},"useSessionStorage",": Reactive Persistent State",[24,743,745],{"className":64,"code":744,"language":66,"meta":29,"style":29},"import { useLocalStorage } from \"@vueuse/core\"\n\n// Replaces localStorage.getItem / setItem / JSON.parse / JSON.stringify\nconst filters = useLocalStorage(\"certificate-filters\", {\n  status: \"ACTIVE\",\n  technology: null,\n  period: null,\n})\n\n// filters is a Ref — any modification is persisted automatically\nfilters.value.status = \"CANCELLED\"\n// localStorage.setItem('certificate-filters', '{\"status\":\"CANCELLED\",...}') called automatically\n",[31,746,747,758,762,767,787,797,806,815,820,824,829,839],{"__ignoreMap":29},[34,748,749,751,754,756],{"class":36,"line":37},[34,750,304],{"class":80},[34,752,753],{"class":94}," { useLocalStorage } ",[34,755,310],{"class":80},[34,757,313],{"class":44},[34,759,760],{"class":36,"line":77},[34,761,145],{"emptyLinePlaceholder":144},[34,763,764],{"class":36,"line":104},[34,765,766],{"class":73},"// Replaces localStorage.getItem / setItem / JSON.parse / JSON.stringify\n",[34,768,769,771,774,776,779,781,784],{"class":36,"line":123},[34,770,81],{"class":80},[34,772,773],{"class":84}," filters",[34,775,88],{"class":80},[34,777,778],{"class":40}," useLocalStorage",[34,780,95],{"class":94},[34,782,783],{"class":44},"\"certificate-filters\"",[34,785,786],{"class":94},", {\n",[34,788,789,792,794],{"class":36,"line":141},[34,790,791],{"class":94},"  status: ",[34,793,482],{"class":44},[34,795,796],{"class":94},",\n",[34,798,799,802,804],{"class":36,"line":148},[34,800,801],{"class":94},"  technology: ",[34,803,98],{"class":84},[34,805,796],{"class":94},[34,807,808,811,813],{"class":36,"line":170},[34,809,810],{"class":94},"  period: ",[34,812,98],{"class":84},[34,814,796],{"class":94},[34,816,817],{"class":36,"line":182},[34,818,819],{"class":94},"})\n",[34,821,822],{"class":36,"line":193},[34,823,145],{"emptyLinePlaceholder":144},[34,825,826],{"class":36,"line":201},[34,827,828],{"class":73},"// filters is a Ref — any modification is persisted automatically\n",[34,830,831,834,836],{"class":36,"line":221},[34,832,833],{"class":94},"filters.value.status ",[34,835,176],{"class":80},[34,837,838],{"class":44}," \"CANCELLED\"\n",[34,840,841],{"class":36,"line":233},[34,842,843],{"class":73},"// localStorage.setItem('certificate-filters', '{\"status\":\"CANCELLED\",...}') called automatically\n",[15,845,846,847,850],{},"VueUse handles JSON serialisation, cross-tab synchronisation (via the ",[31,848,849],{},"storage"," event), and default values when the key does not yet exist.",[15,852,853],{},"With an explicit type for autocomplete:",[24,855,857],{"className":64,"code":856,"language":66,"meta":29,"style":29},"interface FilterState {\n  status: \"ACTIVE\" | \"CANCELLED\" | \"TRANSFERRED\" | null\n  technology: string | null\n  period: string | null\n}\n\nconst filters = useLocalStorage\u003CFilterState>(\"certificate-filters\", {\n  status: null,\n  technology: null,\n  period: null,\n})\n",[31,858,859,869,894,907,920,924,928,951,959,967,975],{"__ignoreMap":29},[34,860,861,864,867],{"class":36,"line":37},[34,862,863],{"class":80},"interface",[34,865,866],{"class":40}," FilterState",[34,868,167],{"class":94},[34,870,871,874,876,879,882,885,887,890,892],{"class":36,"line":77},[34,872,873],{"class":417},"  status",[34,875,294],{"class":80},[34,877,878],{"class":44}," \"ACTIVE\"",[34,880,881],{"class":80}," |",[34,883,884],{"class":44}," \"CANCELLED\"",[34,886,881],{"class":80},[34,888,889],{"class":44}," \"TRANSFERRED\"",[34,891,881],{"class":80},[34,893,190],{"class":84},[34,895,896,899,901,903,905],{"class":36,"line":104},[34,897,898],{"class":417},"  technology",[34,900,294],{"class":80},[34,902,584],{"class":84},[34,904,881],{"class":80},[34,906,190],{"class":84},[34,908,909,912,914,916,918],{"class":36,"line":123},[34,910,911],{"class":417},"  period",[34,913,294],{"class":80},[34,915,584],{"class":84},[34,917,881],{"class":80},[34,919,190],{"class":84},[34,921,922],{"class":36,"line":141},[34,923,274],{"class":94},[34,925,926],{"class":36,"line":148},[34,927,145],{"emptyLinePlaceholder":144},[34,929,930,932,934,936,938,941,944,947,949],{"class":36,"line":170},[34,931,81],{"class":80},[34,933,773],{"class":84},[34,935,88],{"class":80},[34,937,778],{"class":40},[34,939,940],{"class":94},"\u003C",[34,942,943],{"class":40},"FilterState",[34,945,946],{"class":94},">(",[34,948,783],{"class":44},[34,950,786],{"class":94},[34,952,953,955,957],{"class":36,"line":182},[34,954,791],{"class":94},[34,956,98],{"class":84},[34,958,796],{"class":94},[34,960,961,963,965],{"class":36,"line":193},[34,962,801],{"class":94},[34,964,98],{"class":84},[34,966,796],{"class":94},[34,968,969,971,973],{"class":36,"line":201},[34,970,810],{"class":94},[34,972,98],{"class":84},[34,974,796],{"class":94},[34,976,977],{"class":36,"line":221},[34,978,819],{"class":94},[15,980,981,982,984,985,988,989,991,992,995],{},"The pitfall: ",[31,983,737],{}," is not available server-side (SSR/Nuxt). Use ",[31,986,987],{},"import.meta.client"," or the ",[31,990,737],{}," wrapper from ",[31,993,994],{},"@vueuse/nuxt",", which handles SSR correctly.",[19,997,999,1002],{"id":998},"useintersectionobserver-lazy-loading-and-scroll-animations",[31,1000,1001],{},"useIntersectionObserver",": Lazy Loading and Scroll Animations",[15,1004,1005],{},"To load data only when an element enters the viewport:",[24,1007,1009],{"className":64,"code":1008,"language":66,"meta":29,"style":29},"import { useIntersectionObserver } from \"@vueuse/core\"\nimport { ref } from \"vue\"\n\nconst target = ref\u003CHTMLElement | null>(null)\nconst dataLoaded = ref(false)\n\nconst { stop } = useIntersectionObserver(\n  target,\n  ([{ isIntersecting }]) => {\n    if (isIntersecting && !dataLoaded.value) {\n      loadHeavyData()\n      dataLoaded.value = true\n      stop() // Observe only once\n    }\n  },\n  { threshold: 0.1 }, // Trigger when 10% of the element is visible\n)\n",[31,1010,1011,1022,1034,1038,1065,1082,1086,1104,1109,1124,1141,1148,1157,1168,1173,1177,1191],{"__ignoreMap":29},[34,1012,1013,1015,1018,1020],{"class":36,"line":37},[34,1014,304],{"class":80},[34,1016,1017],{"class":94}," { useIntersectionObserver } ",[34,1019,310],{"class":80},[34,1021,313],{"class":44},[34,1023,1024,1026,1029,1031],{"class":36,"line":77},[34,1025,304],{"class":80},[34,1027,1028],{"class":94}," { ref } ",[34,1030,310],{"class":80},[34,1032,1033],{"class":44}," \"vue\"\n",[34,1035,1036],{"class":36,"line":104},[34,1037,145],{"emptyLinePlaceholder":144},[34,1039,1040,1042,1045,1047,1049,1051,1054,1056,1059,1061,1063],{"class":36,"line":123},[34,1041,81],{"class":80},[34,1043,1044],{"class":84}," target",[34,1046,88],{"class":80},[34,1048,91],{"class":40},[34,1050,940],{"class":94},[34,1052,1053],{"class":40},"HTMLElement",[34,1055,881],{"class":80},[34,1057,1058],{"class":84}," null",[34,1060,946],{"class":94},[34,1062,98],{"class":84},[34,1064,101],{"class":94},[34,1066,1067,1069,1072,1074,1076,1078,1080],{"class":36,"line":141},[34,1068,81],{"class":80},[34,1070,1071],{"class":84}," dataLoaded",[34,1073,88],{"class":80},[34,1075,91],{"class":40},[34,1077,95],{"class":94},[34,1079,118],{"class":84},[34,1081,101],{"class":94},[34,1083,1084],{"class":36,"line":148},[34,1085,145],{"emptyLinePlaceholder":144},[34,1087,1088,1090,1092,1095,1097,1099,1102],{"class":36,"line":170},[34,1089,81],{"class":80},[34,1091,324],{"class":94},[34,1093,1094],{"class":84},"stop",[34,1096,346],{"class":94},[34,1098,176],{"class":80},[34,1100,1101],{"class":40}," useIntersectionObserver",[34,1103,354],{"class":94},[34,1105,1106],{"class":36,"line":182},[34,1107,1108],{"class":94},"  target,\n",[34,1110,1111,1114,1117,1120,1122],{"class":36,"line":193},[34,1112,1113],{"class":94},"  ([{ ",[34,1115,1116],{"class":417},"isIntersecting",[34,1118,1119],{"class":94}," }]) ",[34,1121,164],{"class":80},[34,1123,167],{"class":94},[34,1125,1126,1129,1132,1135,1138],{"class":36,"line":201},[34,1127,1128],{"class":80},"    if",[34,1130,1131],{"class":94}," (isIntersecting ",[34,1133,1134],{"class":80},"&&",[34,1136,1137],{"class":80}," !",[34,1139,1140],{"class":94},"dataLoaded.value) {\n",[34,1142,1143,1146],{"class":36,"line":221},[34,1144,1145],{"class":40},"      loadHeavyData",[34,1147,218],{"class":94},[34,1149,1150,1153,1155],{"class":36,"line":233},[34,1151,1152],{"class":94},"      dataLoaded.value ",[34,1154,176],{"class":80},[34,1156,179],{"class":84},[34,1158,1159,1162,1165],{"class":36,"line":244},[34,1160,1161],{"class":40},"      stop",[34,1163,1164],{"class":94},"() ",[34,1166,1167],{"class":73},"// Observe only once\n",[34,1169,1170],{"class":36,"line":254},[34,1171,1172],{"class":94},"    }\n",[34,1174,1175],{"class":36,"line":265},[34,1176,441],{"class":94},[34,1178,1179,1182,1185,1188],{"class":36,"line":271},[34,1180,1181],{"class":94},"  { threshold: ",[34,1183,1184],{"class":84},"0.1",[34,1186,1187],{"class":94}," }, ",[34,1189,1190],{"class":73},"// Trigger when 10% of the element is visible\n",[34,1192,1193],{"class":36,"line":277},[34,1194,101],{"class":94},[15,1196,1197],{},"In the template:",[24,1199,1203],{"className":1200,"code":1201,"language":1202,"meta":29,"style":29},"language-vue shiki shiki-themes github-dark github-light","\u003Ctemplate>\n  \u003Cdiv ref=\"target\">\n    \u003CSpinner v-if=\"!dataLoaded\" />\n    \u003CHeavyChart v-else :data=\"chartData\" />\n  \u003C/div>\n\u003C/template>\n","vue",[31,1204,1205,1216,1233,1252,1272,1281],{"__ignoreMap":29},[34,1206,1207,1209,1213],{"class":36,"line":37},[34,1208,940],{"class":94},[34,1210,1212],{"class":1211},"sZkSk","template",[34,1214,1215],{"class":94},">\n",[34,1217,1218,1221,1224,1226,1228,1231],{"class":36,"line":77},[34,1219,1220],{"class":94},"  \u003C",[34,1222,1223],{"class":1211},"div",[34,1225,91],{"class":40},[34,1227,176],{"class":94},[34,1229,1230],{"class":44},"\"target\"",[34,1232,1215],{"class":94},[34,1234,1235,1238,1241,1244,1246,1249],{"class":36,"line":104},[34,1236,1237],{"class":94},"    \u003C",[34,1239,1240],{"class":1211},"Spinner",[34,1242,1243],{"class":40}," v-if",[34,1245,176],{"class":94},[34,1247,1248],{"class":44},"\"!dataLoaded\"",[34,1250,1251],{"class":94}," />\n",[34,1253,1254,1256,1259,1262,1265,1267,1270],{"class":36,"line":123},[34,1255,1237],{"class":94},[34,1257,1258],{"class":1211},"HeavyChart",[34,1260,1261],{"class":40}," v-else",[34,1263,1264],{"class":40}," :data",[34,1266,176],{"class":94},[34,1268,1269],{"class":44},"\"chartData\"",[34,1271,1251],{"class":94},[34,1273,1274,1277,1279],{"class":36,"line":141},[34,1275,1276],{"class":94},"  \u003C/",[34,1278,1223],{"class":1211},[34,1280,1215],{"class":94},[34,1282,1283,1286,1288],{"class":36,"line":148},[34,1284,1285],{"class":94},"\u003C/",[34,1287,1212],{"class":1211},[34,1289,1215],{"class":94},[15,1291,1292,1295],{},[31,1293,1294],{},"stop()"," halts observation after the first trigger — avoids unnecessary repeated calls. Also useful for entrance animations: applying a CSS class when an element becomes visible.",[19,1297,1299,1302],{"id":1298},"useeventlistener-clean-dom-event-management",[31,1300,1301],{},"useEventListener",": Clean DOM Event Management",[24,1304,1306],{"className":64,"code":1305,"language":66,"meta":29,"style":29},"import { useEventListener } from \"@vueuse/core\"\n\n// Automatically cleaned up when the component unmounts\nuseEventListener(window, \"keydown\", (event: KeyboardEvent) => {\n  if (event.key === \"Escape\") closeModal()\n  if (event.ctrlKey && event.key === \"s\") saveForm()\n})\n\n// On a reactive element ref\nconst tableRef = ref\u003CHTMLElement | null>(null)\nuseEventListener(tableRef, \"click\", handleCellClick)\n",[31,1307,1308,1319,1323,1328,1354,1374,1398,1402,1406,1411,1436],{"__ignoreMap":29},[34,1309,1310,1312,1315,1317],{"class":36,"line":37},[34,1311,304],{"class":80},[34,1313,1314],{"class":94}," { useEventListener } ",[34,1316,310],{"class":80},[34,1318,313],{"class":44},[34,1320,1321],{"class":36,"line":77},[34,1322,145],{"emptyLinePlaceholder":144},[34,1324,1325],{"class":36,"line":104},[34,1326,1327],{"class":73},"// Automatically cleaned up when the component unmounts\n",[34,1329,1330,1332,1335,1338,1341,1343,1345,1348,1350,1352],{"class":36,"line":123},[34,1331,1301],{"class":40},[34,1333,1334],{"class":94},"(window, ",[34,1336,1337],{"class":44},"\"keydown\"",[34,1339,1340],{"class":94},", (",[34,1342,697],{"class":417},[34,1344,294],{"class":80},[34,1346,1347],{"class":40}," KeyboardEvent",[34,1349,421],{"class":94},[34,1351,164],{"class":80},[34,1353,167],{"class":94},[34,1355,1356,1358,1361,1364,1367,1369,1372],{"class":36,"line":141},[34,1357,595],{"class":80},[34,1359,1360],{"class":94}," (event.key ",[34,1362,1363],{"class":80},"===",[34,1365,1366],{"class":44}," \"Escape\"",[34,1368,421],{"class":94},[34,1370,1371],{"class":40},"closeModal",[34,1373,218],{"class":94},[34,1375,1376,1378,1381,1383,1386,1388,1391,1393,1396],{"class":36,"line":148},[34,1377,595],{"class":80},[34,1379,1380],{"class":94}," (event.ctrlKey ",[34,1382,1134],{"class":80},[34,1384,1385],{"class":94}," event.key ",[34,1387,1363],{"class":80},[34,1389,1390],{"class":44}," \"s\"",[34,1392,421],{"class":94},[34,1394,1395],{"class":40},"saveForm",[34,1397,218],{"class":94},[34,1399,1400],{"class":36,"line":170},[34,1401,819],{"class":94},[34,1403,1404],{"class":36,"line":182},[34,1405,145],{"emptyLinePlaceholder":144},[34,1407,1408],{"class":36,"line":193},[34,1409,1410],{"class":73},"// On a reactive element ref\n",[34,1412,1413,1415,1418,1420,1422,1424,1426,1428,1430,1432,1434],{"class":36,"line":201},[34,1414,81],{"class":80},[34,1416,1417],{"class":84}," tableRef",[34,1419,88],{"class":80},[34,1421,91],{"class":40},[34,1423,940],{"class":94},[34,1425,1053],{"class":40},[34,1427,881],{"class":80},[34,1429,1058],{"class":84},[34,1431,946],{"class":94},[34,1433,98],{"class":84},[34,1435,101],{"class":94},[34,1437,1438,1440,1443,1446],{"class":36,"line":221},[34,1439,1301],{"class":40},[34,1441,1442],{"class":94},"(tableRef, ",[34,1444,1445],{"class":44},"\"click\"",[34,1447,1448],{"class":94},", handleCellClick)\n",[15,1450,1451,1452,1455,1456,1459,1460,1462],{},"Without VueUse, you must remember to call ",[31,1453,1454],{},"removeEventListener"," in ",[31,1457,1458],{},"onUnmounted"," — easy to forget, and a reliable source of memory leaks. ",[31,1461,1301],{}," handles this automatically.",[19,1464,1466,1469],{"id":1465},"useclipboard-copying-to-the-clipboard",[31,1467,1468],{},"useClipboard",": Copying to the Clipboard",[24,1471,1473],{"className":64,"code":1472,"language":66,"meta":29,"style":29},"import { useClipboard } from \"@vueuse/core\"\n\nconst { copy, copied, isSupported } = useClipboard()\n",[31,1474,1475,1486,1490],{"__ignoreMap":29},[34,1476,1477,1479,1482,1484],{"class":36,"line":37},[34,1478,304],{"class":80},[34,1480,1481],{"class":94}," { useClipboard } ",[34,1483,310],{"class":80},[34,1485,313],{"class":44},[34,1487,1488],{"class":36,"line":77},[34,1489,145],{"emptyLinePlaceholder":144},[34,1491,1492,1494,1496,1499,1501,1504,1506,1509,1511,1513,1516],{"class":36,"line":104},[34,1493,81],{"class":80},[34,1495,324],{"class":94},[34,1497,1498],{"class":84},"copy",[34,1500,330],{"class":94},[34,1502,1503],{"class":84},"copied",[34,1505,330],{"class":94},[34,1507,1508],{"class":84},"isSupported",[34,1510,346],{"class":94},[34,1512,176],{"class":80},[34,1514,1515],{"class":40}," useClipboard",[34,1517,218],{"class":94},[24,1519,1521],{"className":1200,"code":1520,"language":1202,"meta":29,"style":29},"\u003Ctemplate>\n  \u003Cbutton @click=\"copy(certificateId)\" :disabled=\"!isSupported\">\n    {{ copied ? \"✓ Copied\" : \"Copy ID\" }}\n  \u003C/button>\n\u003C/template>\n",[31,1522,1523,1531,1556,1561,1569],{"__ignoreMap":29},[34,1524,1525,1527,1529],{"class":36,"line":37},[34,1526,940],{"class":94},[34,1528,1212],{"class":1211},[34,1530,1215],{"class":94},[34,1532,1533,1535,1538,1541,1543,1546,1549,1551,1554],{"class":36,"line":77},[34,1534,1220],{"class":94},[34,1536,1537],{"class":1211},"button",[34,1539,1540],{"class":40}," @click",[34,1542,176],{"class":94},[34,1544,1545],{"class":44},"\"copy(certificateId)\"",[34,1547,1548],{"class":40}," :disabled",[34,1550,176],{"class":94},[34,1552,1553],{"class":44},"\"!isSupported\"",[34,1555,1215],{"class":94},[34,1557,1558],{"class":36,"line":104},[34,1559,1560],{"class":94},"    {{ copied ? \"✓ Copied\" : \"Copy ID\" }}\n",[34,1562,1563,1565,1567],{"class":36,"line":123},[34,1564,1276],{"class":94},[34,1566,1537],{"class":1211},[34,1568,1215],{"class":94},[34,1570,1571,1573,1575],{"class":36,"line":141},[34,1572,1285],{"class":94},[34,1574,1212],{"class":1211},[34,1576,1215],{"class":94},[15,1578,1579,1581,1582,1584,1585,1587],{},[31,1580,1503],{}," automatically reverts to ",[31,1583,118],{}," after 1.5 seconds (configurable). ",[31,1586,1508],{}," checks whether the Clipboard API is available in the browser — useful for providing a fallback.",[19,1589,1591,1594],{"id":1590},"usemediaquery-responsive-logic-beyond-css",[31,1592,1593],{},"useMediaQuery",": Responsive Logic Beyond CSS",[24,1596,1598],{"className":64,"code":1597,"language":66,"meta":29,"style":29},"import { useMediaQuery } from \"@vueuse/core\"\n\nconst isMobile = useMediaQuery(\"(max-width: 768px)\")\nconst prefersReducedMotion = useMediaQuery(\"(prefers-reduced-motion: reduce)\")\nconst isDarkMode = useMediaQuery(\"(prefers-color-scheme: dark)\")\n\n// Reactive — updates when the window is resized\nwatch(isMobile, (mobile) => {\n  if (mobile) collapseNavigation()\n})\n",[31,1599,1600,1611,1615,1634,1652,1670,1674,1679,1695,1707],{"__ignoreMap":29},[34,1601,1602,1604,1607,1609],{"class":36,"line":37},[34,1603,304],{"class":80},[34,1605,1606],{"class":94}," { useMediaQuery } ",[34,1608,310],{"class":80},[34,1610,313],{"class":44},[34,1612,1613],{"class":36,"line":77},[34,1614,145],{"emptyLinePlaceholder":144},[34,1616,1617,1619,1622,1624,1627,1629,1632],{"class":36,"line":104},[34,1618,81],{"class":80},[34,1620,1621],{"class":84}," isMobile",[34,1623,88],{"class":80},[34,1625,1626],{"class":40}," useMediaQuery",[34,1628,95],{"class":94},[34,1630,1631],{"class":44},"\"(max-width: 768px)\"",[34,1633,101],{"class":94},[34,1635,1636,1638,1641,1643,1645,1647,1650],{"class":36,"line":123},[34,1637,81],{"class":80},[34,1639,1640],{"class":84}," prefersReducedMotion",[34,1642,88],{"class":80},[34,1644,1626],{"class":40},[34,1646,95],{"class":94},[34,1648,1649],{"class":44},"\"(prefers-reduced-motion: reduce)\"",[34,1651,101],{"class":94},[34,1653,1654,1656,1659,1661,1663,1665,1668],{"class":36,"line":141},[34,1655,81],{"class":80},[34,1657,1658],{"class":84}," isDarkMode",[34,1660,88],{"class":80},[34,1662,1626],{"class":40},[34,1664,95],{"class":94},[34,1666,1667],{"class":44},"\"(prefers-color-scheme: dark)\"",[34,1669,101],{"class":94},[34,1671,1672],{"class":36,"line":148},[34,1673,145],{"emptyLinePlaceholder":144},[34,1675,1676],{"class":36,"line":170},[34,1677,1678],{"class":73},"// Reactive — updates when the window is resized\n",[34,1680,1681,1683,1686,1689,1691,1693],{"class":36,"line":182},[34,1682,651],{"class":40},[34,1684,1685],{"class":94},"(isMobile, (",[34,1687,1688],{"class":417},"mobile",[34,1690,421],{"class":94},[34,1692,164],{"class":80},[34,1694,167],{"class":94},[34,1696,1697,1699,1702,1705],{"class":36,"line":193},[34,1698,595],{"class":80},[34,1700,1701],{"class":94}," (mobile) ",[34,1703,1704],{"class":40},"collapseNavigation",[34,1706,218],{"class":94},[34,1708,1709],{"class":36,"line":201},[34,1710,819],{"class":94},[15,1712,1713],{},"Useful when JavaScript behaviour must adapt to screen size — not just CSS. For instance, disabling complex animations on mobile, or reducing the amount of data loaded on smaller viewports.",[19,1715,1717,1720],{"id":1716},"useeventsource-consuming-an-sse-stream",[31,1718,1719],{},"useEventSource",": Consuming an SSE Stream",[15,1722,1723],{},"Server-Sent Events is often preferable to WebSockets for unidirectional streams (notifications, status updates) — simpler, with native automatic reconnection, and compatible with HTTP proxies.",[24,1725,1727],{"className":64,"code":1726,"language":66,"meta":29,"style":29},"import { useEventSource } from \"@vueuse/core\"\n\nconst { data, status, error, close } = useEventSource(\n  \"/api/events/certificates\",\n  [\"certificate_updated\", \"certificate_created\"], // Events to listen to\n  { withCredentials: true },\n)\n\n// data holds the most recently received payload\nwatch(data, (raw) => {\n  if (!raw) return\n  const event = JSON.parse(raw)\n  updateCertificateInList(event)\n})\n\n// status: 'CONNECTING' | 'OPEN' | 'CLOSED'\n",[31,1728,1729,1740,1744,1776,1783,1802,1812,1816,1820,1825,1841,1855,1877,1885,1889,1893],{"__ignoreMap":29},[34,1730,1731,1733,1736,1738],{"class":36,"line":37},[34,1732,304],{"class":80},[34,1734,1735],{"class":94}," { useEventSource } ",[34,1737,310],{"class":80},[34,1739,313],{"class":44},[34,1741,1742],{"class":36,"line":77},[34,1743,145],{"emptyLinePlaceholder":144},[34,1745,1746,1748,1750,1753,1755,1758,1760,1762,1764,1767,1769,1771,1774],{"class":36,"line":104},[34,1747,81],{"class":80},[34,1749,324],{"class":94},[34,1751,1752],{"class":84},"data",[34,1754,330],{"class":94},[34,1756,1757],{"class":84},"status",[34,1759,330],{"class":94},[34,1761,338],{"class":84},[34,1763,330],{"class":94},[34,1765,1766],{"class":84},"close",[34,1768,346],{"class":94},[34,1770,176],{"class":80},[34,1772,1773],{"class":40}," useEventSource",[34,1775,354],{"class":94},[34,1777,1778,1781],{"class":36,"line":123},[34,1779,1780],{"class":44},"  \"/api/events/certificates\"",[34,1782,796],{"class":94},[34,1784,1785,1788,1791,1793,1796,1799],{"class":36,"line":141},[34,1786,1787],{"class":94},"  [",[34,1789,1790],{"class":44},"\"certificate_updated\"",[34,1792,330],{"class":94},[34,1794,1795],{"class":44},"\"certificate_created\"",[34,1797,1798],{"class":94},"], ",[34,1800,1801],{"class":73},"// Events to listen to\n",[34,1803,1804,1807,1809],{"class":36,"line":148},[34,1805,1806],{"class":94},"  { withCredentials: ",[34,1808,389],{"class":84},[34,1810,1811],{"class":94}," },\n",[34,1813,1814],{"class":36,"line":170},[34,1815,101],{"class":94},[34,1817,1818],{"class":36,"line":182},[34,1819,145],{"emptyLinePlaceholder":144},[34,1821,1822],{"class":36,"line":193},[34,1823,1824],{"class":73},"// data holds the most recently received payload\n",[34,1826,1827,1829,1832,1835,1837,1839],{"class":36,"line":201},[34,1828,651],{"class":40},[34,1830,1831],{"class":94},"(data, (",[34,1833,1834],{"class":417},"raw",[34,1836,421],{"class":94},[34,1838,164],{"class":80},[34,1840,167],{"class":94},[34,1842,1843,1845,1847,1850,1853],{"class":36,"line":221},[34,1844,595],{"class":80},[34,1846,576],{"class":94},[34,1848,1849],{"class":80},"!",[34,1851,1852],{"class":94},"raw) ",[34,1854,612],{"class":80},[34,1856,1857,1860,1863,1865,1868,1871,1874],{"class":36,"line":233},[34,1858,1859],{"class":80},"  const",[34,1861,1862],{"class":84}," event",[34,1864,88],{"class":80},[34,1866,1867],{"class":84}," JSON",[34,1869,1870],{"class":94},".",[34,1872,1873],{"class":40},"parse",[34,1875,1876],{"class":94},"(raw)\n",[34,1878,1879,1882],{"class":36,"line":244},[34,1880,1881],{"class":40},"  updateCertificateInList",[34,1883,1884],{"class":94},"(event)\n",[34,1886,1887],{"class":36,"line":254},[34,1888,819],{"class":94},[34,1890,1891],{"class":36,"line":265},[34,1892,145],{"emptyLinePlaceholder":144},[34,1894,1895],{"class":36,"line":271},[34,1896,1897],{"class":73},"// status: 'CONNECTING' | 'OPEN' | 'CLOSED'\n",[15,1899,1900],{},"On the FastAPI side, a minimal SSE endpoint:",[24,1902,1906],{"className":1903,"code":1904,"language":1905,"meta":29,"style":29},"language-python shiki shiki-themes github-dark github-light","from fastapi.responses import StreamingResponse\nimport asyncio\nimport json\n\n@router.get(\"/api/events/certificates\")\nasync def certificate_events(request: Request):\n    async def event_generator():\n        while True:\n            if await request.is_disconnected():\n                break\n            event = await event_queue.get()\n            yield f\"event: {event['type']}\\ndata: {json.dumps(event)}\\n\\n\"\n\n    return StreamingResponse(\n        event_generator(),\n        media_type=\"text/event-stream\",\n        headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\"}\n    )\n","python",[31,1907,1908,1913,1918,1923,1927,1932,1937,1942,1947,1952,1957,1962,1967,1971,1976,1981,1986,1991],{"__ignoreMap":29},[34,1909,1910],{"class":36,"line":37},[34,1911,1912],{},"from fastapi.responses import StreamingResponse\n",[34,1914,1915],{"class":36,"line":77},[34,1916,1917],{},"import asyncio\n",[34,1919,1920],{"class":36,"line":104},[34,1921,1922],{},"import json\n",[34,1924,1925],{"class":36,"line":123},[34,1926,145],{"emptyLinePlaceholder":144},[34,1928,1929],{"class":36,"line":141},[34,1930,1931],{},"@router.get(\"/api/events/certificates\")\n",[34,1933,1934],{"class":36,"line":148},[34,1935,1936],{},"async def certificate_events(request: Request):\n",[34,1938,1939],{"class":36,"line":170},[34,1940,1941],{},"    async def event_generator():\n",[34,1943,1944],{"class":36,"line":182},[34,1945,1946],{},"        while True:\n",[34,1948,1949],{"class":36,"line":193},[34,1950,1951],{},"            if await request.is_disconnected():\n",[34,1953,1954],{"class":36,"line":201},[34,1955,1956],{},"                break\n",[34,1958,1959],{"class":36,"line":221},[34,1960,1961],{},"            event = await event_queue.get()\n",[34,1963,1964],{"class":36,"line":233},[34,1965,1966],{},"            yield f\"event: {event['type']}\\ndata: {json.dumps(event)}\\n\\n\"\n",[34,1968,1969],{"class":36,"line":244},[34,1970,145],{"emptyLinePlaceholder":144},[34,1972,1973],{"class":36,"line":254},[34,1974,1975],{},"    return StreamingResponse(\n",[34,1977,1978],{"class":36,"line":265},[34,1979,1980],{},"        event_generator(),\n",[34,1982,1983],{"class":36,"line":271},[34,1984,1985],{},"        media_type=\"text/event-stream\",\n",[34,1987,1988],{"class":36,"line":277},[34,1989,1990],{},"        headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\"}\n",[34,1992,1993],{"class":36,"line":282},[34,1994,1995],{},"    )\n",[15,1997,1998,2001],{},[31,1999,2000],{},"X-Accel-Buffering: no"," is critical behind nginx or an OpenShift ingress — without it, events are buffered and do not arrive in real time.",[19,2003,2005,2008],{"id":2004},"usevmodel-simplifying-form-components",[31,2006,2007],{},"useVModel",": Simplifying Form Components",[15,2010,2011,2012,294],{},"For a component that wraps an input and needs to support ",[31,2013,2014],{},"v-model",[24,2016,2018],{"className":64,"code":2017,"language":66,"meta":29,"style":29},"import { useVModel } from \"@vueuse/core\"\n\n// InputField.vue\nconst props = defineProps\u003C{\n  modelValue: string\n  label: string\n}>()\nconst emit = defineEmits([\"update:modelValue\"])\n\nconst value = useVModel(props, \"modelValue\", emit)\n\n// value is a writable Ref — usable directly in the template\n",[31,2019,2020,2031,2035,2040,2055,2065,2074,2079,2100,2104,2125,2129],{"__ignoreMap":29},[34,2021,2022,2024,2027,2029],{"class":36,"line":37},[34,2023,304],{"class":80},[34,2025,2026],{"class":94}," { useVModel } ",[34,2028,310],{"class":80},[34,2030,313],{"class":44},[34,2032,2033],{"class":36,"line":77},[34,2034,145],{"emptyLinePlaceholder":144},[34,2036,2037],{"class":36,"line":104},[34,2038,2039],{"class":73},"// InputField.vue\n",[34,2041,2042,2044,2047,2049,2052],{"class":36,"line":123},[34,2043,81],{"class":80},[34,2045,2046],{"class":84}," props",[34,2048,88],{"class":80},[34,2050,2051],{"class":40}," defineProps",[34,2053,2054],{"class":94},"\u003C{\n",[34,2056,2057,2060,2062],{"class":36,"line":141},[34,2058,2059],{"class":417},"  modelValue",[34,2061,294],{"class":80},[34,2063,2064],{"class":84}," string\n",[34,2066,2067,2070,2072],{"class":36,"line":148},[34,2068,2069],{"class":417},"  label",[34,2071,294],{"class":80},[34,2073,2064],{"class":84},[34,2075,2076],{"class":36,"line":170},[34,2077,2078],{"class":94},"}>()\n",[34,2080,2081,2083,2086,2088,2091,2094,2097],{"class":36,"line":182},[34,2082,81],{"class":80},[34,2084,2085],{"class":84}," emit",[34,2087,88],{"class":80},[34,2089,2090],{"class":40}," defineEmits",[34,2092,2093],{"class":94},"([",[34,2095,2096],{"class":44},"\"update:modelValue\"",[34,2098,2099],{"class":94},"])\n",[34,2101,2102],{"class":36,"line":193},[34,2103,145],{"emptyLinePlaceholder":144},[34,2105,2106,2108,2111,2113,2116,2119,2122],{"class":36,"line":201},[34,2107,81],{"class":80},[34,2109,2110],{"class":84}," value",[34,2112,88],{"class":80},[34,2114,2115],{"class":40}," useVModel",[34,2117,2118],{"class":94},"(props, ",[34,2120,2121],{"class":44},"\"modelValue\"",[34,2123,2124],{"class":94},", emit)\n",[34,2126,2127],{"class":36,"line":221},[34,2128,145],{"emptyLinePlaceholder":144},[34,2130,2131],{"class":36,"line":233},[34,2132,2133],{"class":73},"// value is a writable Ref — usable directly in the template\n",[24,2135,2137],{"className":1200,"code":2136,"language":1202,"meta":29,"style":29},"\u003Ctemplate>\n  \u003Cdiv>\n    \u003Clabel>{{ label }}\u003C/label>\n    \u003Cinput v-model=\"value\" />\n  \u003C/div>\n\u003C/template>\n",[31,2138,2139,2147,2155,2169,2186,2194],{"__ignoreMap":29},[34,2140,2141,2143,2145],{"class":36,"line":37},[34,2142,940],{"class":94},[34,2144,1212],{"class":1211},[34,2146,1215],{"class":94},[34,2148,2149,2151,2153],{"class":36,"line":77},[34,2150,1220],{"class":94},[34,2152,1223],{"class":1211},[34,2154,1215],{"class":94},[34,2156,2157,2159,2162,2165,2167],{"class":36,"line":104},[34,2158,1237],{"class":94},[34,2160,2161],{"class":1211},"label",[34,2163,2164],{"class":94},">{{ label }}\u003C/",[34,2166,2161],{"class":1211},[34,2168,1215],{"class":94},[34,2170,2171,2173,2176,2179,2181,2184],{"class":36,"line":123},[34,2172,1237],{"class":94},[34,2174,2175],{"class":1211},"input",[34,2177,2178],{"class":40}," v-model",[34,2180,176],{"class":94},[34,2182,2183],{"class":44},"\"value\"",[34,2185,1251],{"class":94},[34,2187,2188,2190,2192],{"class":36,"line":141},[34,2189,1276],{"class":94},[34,2191,1223],{"class":1211},[34,2193,1215],{"class":94},[34,2195,2196,2198,2200],{"class":36,"line":148},[34,2197,1285],{"class":94},[34,2199,1212],{"class":1211},[34,2201,1215],{"class":94},[15,2203,2204,2205,2207],{},"Without ",[31,2206,2007],{},", you must manually manage the prop and the emit — two extra lines, and the risk of accidentally mutating the prop directly.",[19,2209,2211],{"id":2210},"key-takeaways","Key Takeaways",[15,2213,2214,2215,330,2217,2219,2220,330,2222,330,2224,2226,2227,330,2229,2231],{},"VueUse is most valuable across three categories: composables that eliminate recurring boilerplate (",[31,2216,57],{},[31,2218,2007],{},"), composables that wrap verbose browser APIs (",[31,2221,1001],{},[31,2223,1301],{},[31,2225,1468],{},"), and composables that address performance concerns (",[31,2228,506],{},[31,2230,510],{},"). The rest is situationally useful — but these ten appear on virtually every professional Vue.js project.",[2233,2234,2235],"style",{},"html pre.shiki code .s-Z4r, html code.shiki .s-Z4r{--shiki-dark:#B392F0;--shiki-default:#6F42C1}html pre.shiki code .sg6BJ, html code.shiki .sg6BJ{--shiki-dark:#9ECBFF;--shiki-default:#032F62}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}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);}html pre.shiki code .sryI4, html code.shiki .sryI4{--shiki-dark:#6A737D;--shiki-default:#6A737D}html pre.shiki code .scx8i, html code.shiki .scx8i{--shiki-dark:#F97583;--shiki-default:#D73A49}html pre.shiki code .s0DvM, html code.shiki .s0DvM{--shiki-dark:#79B8FF;--shiki-default:#005CC5}html pre.shiki code .sQ3_J, html code.shiki .sQ3_J{--shiki-dark:#E1E4E8;--shiki-default:#24292E}html pre.shiki code .sFbx2, html code.shiki .sFbx2{--shiki-dark:#FFAB70;--shiki-default:#E36209}html pre.shiki code .sZkSk, html code.shiki .sZkSk{--shiki-dark:#85E89D;--shiki-default:#22863A}",{"title":29,"searchDepth":77,"depth":77,"links":2237},[2238,2239,2241,2243,2245,2247,2249,2251,2253,2255,2257],{"id":21,"depth":77,"text":22},{"id":54,"depth":77,"text":2240},"useAsyncState: Replacing the loading/error/data Pattern",{"id":503,"depth":77,"text":2242},"useDebounceFn and useThrottleFn: Performance on Frequent Events",{"id":734,"depth":77,"text":2244},"useLocalStorage and useSessionStorage: Reactive Persistent State",{"id":998,"depth":77,"text":2246},"useIntersectionObserver: Lazy Loading and Scroll Animations",{"id":1298,"depth":77,"text":2248},"useEventListener: Clean DOM Event Management",{"id":1465,"depth":77,"text":2250},"useClipboard: Copying to the Clipboard",{"id":1590,"depth":77,"text":2252},"useMediaQuery: Responsive Logic Beyond CSS",{"id":1716,"depth":77,"text":2254},"useEventSource: Consuming an SSE Stream",{"id":2004,"depth":77,"text":2256},"useVModel: Simplifying Form Components",{"id":2210,"depth":77,"text":2211},"2025-01-10",null,"md",{},"/en/blog/vueuse-essentiels",{"title":5,"description":17},"vueuse-essentiels","en/blog/vueuse-essentiels",[2267,2268,2269,2270],"Vue.js","VueUse","Composition API","Frontend","g-rrYJc07LY3uzlMGHOyWf6cggd0-6YCVVZ09GfnLXg",1774645635810]