[{"data":1,"prerenderedAt":2286},["ShallowReactive",2],{"post-fr-vueuse-essentiels":3},{"id":4,"title":5,"body":6,"date":2272,"description":17,"excerpt":2273,"extension":2274,"meta":2275,"navigation":144,"path":2276,"readTime":201,"seo":2277,"slug":2278,"stem":2279,"tags":2280,"__hash__":2285},"fr_blog/fr/blog/vueuse-essentiels.md","VueUse : les composables qui changent le quotidien",{"type":7,"value":8,"toc":2250},"minimark",[9,14,18,23,49,52,59,62,289,295,446,454,492,501,512,515,661,666,735,738,748,850,857,860,985,1002,1009,1012,1201,1204,1296,1302,1309,1455,1469,1476,1532,1591,1602,1609,1725,1728,1735,1738,1912,1915,2010,2016,2023,2029,2148,2216,2222,2226,2246],[10,11,13],"h1",{"id":12},"vueuse-en-production-les-composables-qui-changent-vraiment-le-quotidien","VueUse en production : les composables qui changent vraiment le quotidien",[15,16,17],"p",{},"VueUse compte plus de 200 composables. La documentation les liste tous, ce qui ne t'aide pas à savoir lesquels valent vraiment le coup d'apprendre. Voici ceux qui reviennent systématiquement sur des projets professionnels, avec les situations concrètes où ils font gagner du temps.",[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 est compatible Vue 3 et Nuxt 3/4. Les composables sont tree-shakable — seuls ceux que tu importes sont inclus dans le bundle.",[19,53,55,58],{"id":54},"useasyncstate-remplacer-le-pattern-loadingerrordata",[31,56,57],{},"useAsyncState"," : remplacer le pattern loading/error/data",[15,60,61],{},"Le pattern le plus répétitif en Vue.js :",[24,63,67],{"className":64,"code":65,"language":66,"meta":29,"style":29},"language-typescript shiki shiki-themes github-dark github-light","// Ce qu'on écrit sans VueUse — encore et encore\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","// Ce qu'on écrit sans VueUse — encore et encore\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],{},"Avec ",[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  [], // Valeur initiale\n  {\n    immediate: true, // Exécuter au montage\n    resetOnExecute: true, // Remettre à la valeur initiale avant chaque exécution\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},"// Valeur initiale\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},"// Exécuter au montage\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},"// Remettre à la valeur initiale avant chaque exécution\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],{}," est typé selon la valeur de retour de la fonction async. ",[31,452,343],{}," permet de relancer manuellement avec des paramètres différents :",[24,455,457],{"className":64,"code":456,"language":66,"meta":29,"style":29},"// Relancer avec un filtre différent\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},"// Relancer avec un filtre différent\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],{},"Le deuxième argument de ",[31,496,343],{}," (le délai) est un vestige de l'API — passe ",[31,499,476],{}," pour une exécution immédiate.",[19,502,504,507,508,511],{"id":503},"usedebouncefn-et-usethrottlefn-performances-sur-les-événements-fréquents",[31,505,506],{},"useDebounceFn"," et ",[31,509,510],{},"useThrottleFn"," : performances sur les événements fréquents",[15,513,514],{},"Sur un champ de recherche qui appelle une API à chaque frappe :",[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 après la dernière frappe\n\n// Dans le template\nwatch(search, searchApi)\n",[31,519,520,531,535,553,557,592,614,631,644,648,653],{"__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,583,586,588,590],{"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,582],{"class":80},":",[34,584,585],{"class":84}," string",[34,587,421],{"class":94},[34,589,164],{"class":80},[34,591,167],{"class":94},[34,593,594,597,600,603,606,609,611],{"class":36,"line":148},[34,595,596],{"class":80},"  if",[34,598,599],{"class":94}," (query.",[34,601,602],{"class":84},"length",[34,604,605],{"class":80}," \u003C",[34,607,608],{"class":84}," 2",[34,610,421],{"class":94},[34,612,613],{"class":80},"return\n",[34,615,616,619,621,623,625,628],{"class":36,"line":170},[34,617,618],{"class":94},"  results.value ",[34,620,176],{"class":80},[34,622,209],{"class":80},[34,624,212],{"class":94},[34,626,627],{"class":40},"search",[34,629,630],{"class":94},"(query)\n",[34,632,633,636,639,641],{"class":36,"line":182},[34,634,635],{"class":94},"}, ",[34,637,638],{"class":84},"350",[34,640,421],{"class":94},[34,642,643],{"class":73},"// 350ms après la dernière frappe\n",[34,645,646],{"class":36,"line":193},[34,647,145],{"emptyLinePlaceholder":144},[34,649,650],{"class":36,"line":201},[34,651,652],{"class":73},"// Dans le template\n",[34,654,655,658],{"class":36,"line":221},[34,656,657],{"class":40},"watch",[34,659,660],{"class":94},"(search, searchApi)\n",[15,662,663,665],{},[31,664,510],{}," pour les cas où tu veux garantir une exécution maximum par intervalle (scroll, resize, mousemove) :",[24,667,669],{"className":64,"code":668,"language":66,"meta":29,"style":29},"import { useThrottleFn } from \"@vueuse/core\"\n\nconst onScroll = useThrottleFn((event: Event) => {\n  updateScrollPosition(window.scrollY)\n}, 100) // Maximum 1 exécution par 100ms\n",[31,670,671,682,686,715,723],{"__ignoreMap":29},[34,672,673,675,678,680],{"class":36,"line":37},[34,674,304],{"class":80},[34,676,677],{"class":94}," { useThrottleFn } ",[34,679,310],{"class":80},[34,681,313],{"class":44},[34,683,684],{"class":36,"line":77},[34,685,145],{"emptyLinePlaceholder":144},[34,687,688,690,693,695,698,701,704,706,709,711,713],{"class":36,"line":104},[34,689,81],{"class":80},[34,691,692],{"class":84}," onScroll",[34,694,88],{"class":80},[34,696,697],{"class":40}," useThrottleFn",[34,699,700],{"class":94},"((",[34,702,703],{"class":417},"event",[34,705,582],{"class":80},[34,707,708],{"class":40}," Event",[34,710,421],{"class":94},[34,712,164],{"class":80},[34,714,167],{"class":94},[34,716,717,720],{"class":36,"line":123},[34,718,719],{"class":40},"  updateScrollPosition",[34,721,722],{"class":94},"(window.scrollY)\n",[34,724,725,727,730,732],{"class":36,"line":141},[34,726,635],{"class":94},[34,728,729],{"class":84},"100",[34,731,421],{"class":94},[34,733,734],{"class":73},"// Maximum 1 exécution par 100ms\n",[15,736,737],{},"La différence : debounce attend que l'activité s'arrête, throttle exécute à intervalle régulier pendant l'activité. Règle pratique : debounce pour la recherche, throttle pour le scroll.",[19,739,741,507,744,747],{"id":740},"uselocalstorage-et-usesessionstorage-état-persistant-réactif",[31,742,743],{},"useLocalStorage",[31,745,746],{},"useSessionStorage"," : état persistant réactif",[24,749,751],{"className":64,"code":750,"language":66,"meta":29,"style":29},"import { useLocalStorage } from \"@vueuse/core\"\n\n// Remplace 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 est une Ref — toute modification est persistée automatiquement\nfilters.value.status = \"CANCELLED\"\n// localStorage.setItem('certificate-filters', '{\"status\":\"CANCELLED\",...}') appelé automatiquement\n",[31,752,753,764,768,773,793,803,812,821,826,830,835,845],{"__ignoreMap":29},[34,754,755,757,760,762],{"class":36,"line":37},[34,756,304],{"class":80},[34,758,759],{"class":94}," { useLocalStorage } ",[34,761,310],{"class":80},[34,763,313],{"class":44},[34,765,766],{"class":36,"line":77},[34,767,145],{"emptyLinePlaceholder":144},[34,769,770],{"class":36,"line":104},[34,771,772],{"class":73},"// Remplace localStorage.getItem / setItem / JSON.parse / JSON.stringify\n",[34,774,775,777,780,782,785,787,790],{"class":36,"line":123},[34,776,81],{"class":80},[34,778,779],{"class":84}," filters",[34,781,88],{"class":80},[34,783,784],{"class":40}," useLocalStorage",[34,786,95],{"class":94},[34,788,789],{"class":44},"\"certificate-filters\"",[34,791,792],{"class":94},", {\n",[34,794,795,798,800],{"class":36,"line":141},[34,796,797],{"class":94},"  status: ",[34,799,482],{"class":44},[34,801,802],{"class":94},",\n",[34,804,805,808,810],{"class":36,"line":148},[34,806,807],{"class":94},"  technology: ",[34,809,98],{"class":84},[34,811,802],{"class":94},[34,813,814,817,819],{"class":36,"line":170},[34,815,816],{"class":94},"  period: ",[34,818,98],{"class":84},[34,820,802],{"class":94},[34,822,823],{"class":36,"line":182},[34,824,825],{"class":94},"})\n",[34,827,828],{"class":36,"line":193},[34,829,145],{"emptyLinePlaceholder":144},[34,831,832],{"class":36,"line":201},[34,833,834],{"class":73},"// filters est une Ref — toute modification est persistée automatiquement\n",[34,836,837,840,842],{"class":36,"line":221},[34,838,839],{"class":94},"filters.value.status ",[34,841,176],{"class":80},[34,843,844],{"class":44}," \"CANCELLED\"\n",[34,846,847],{"class":36,"line":233},[34,848,849],{"class":73},"// localStorage.setItem('certificate-filters', '{\"status\":\"CANCELLED\",...}') appelé automatiquement\n",[15,851,852,853,856],{},"VueUse gère la sérialisation JSON, la synchronisation entre onglets (via l'événement ",[31,854,855],{},"storage","), et les valeurs par défaut si la clé n'existe pas encore.",[15,858,859],{},"Avec un type explicite pour l'autocomplétion :",[24,861,863],{"className":64,"code":862,"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,864,865,875,900,913,926,930,934,957,965,973,981],{"__ignoreMap":29},[34,866,867,870,873],{"class":36,"line":37},[34,868,869],{"class":80},"interface",[34,871,872],{"class":40}," FilterState",[34,874,167],{"class":94},[34,876,877,880,882,885,888,891,893,896,898],{"class":36,"line":77},[34,878,879],{"class":417},"  status",[34,881,582],{"class":80},[34,883,884],{"class":44}," \"ACTIVE\"",[34,886,887],{"class":80}," |",[34,889,890],{"class":44}," \"CANCELLED\"",[34,892,887],{"class":80},[34,894,895],{"class":44}," \"TRANSFERRED\"",[34,897,887],{"class":80},[34,899,190],{"class":84},[34,901,902,905,907,909,911],{"class":36,"line":104},[34,903,904],{"class":417},"  technology",[34,906,582],{"class":80},[34,908,585],{"class":84},[34,910,887],{"class":80},[34,912,190],{"class":84},[34,914,915,918,920,922,924],{"class":36,"line":123},[34,916,917],{"class":417},"  period",[34,919,582],{"class":80},[34,921,585],{"class":84},[34,923,887],{"class":80},[34,925,190],{"class":84},[34,927,928],{"class":36,"line":141},[34,929,274],{"class":94},[34,931,932],{"class":36,"line":148},[34,933,145],{"emptyLinePlaceholder":144},[34,935,936,938,940,942,944,947,950,953,955],{"class":36,"line":170},[34,937,81],{"class":80},[34,939,779],{"class":84},[34,941,88],{"class":80},[34,943,784],{"class":40},[34,945,946],{"class":94},"\u003C",[34,948,949],{"class":40},"FilterState",[34,951,952],{"class":94},">(",[34,954,789],{"class":44},[34,956,792],{"class":94},[34,958,959,961,963],{"class":36,"line":182},[34,960,797],{"class":94},[34,962,98],{"class":84},[34,964,802],{"class":94},[34,966,967,969,971],{"class":36,"line":193},[34,968,807],{"class":94},[34,970,98],{"class":84},[34,972,802],{"class":94},[34,974,975,977,979],{"class":36,"line":201},[34,976,816],{"class":94},[34,978,98],{"class":84},[34,980,802],{"class":94},[34,982,983],{"class":36,"line":221},[34,984,825],{"class":94},[15,986,987,988,990,991,994,995,997,998,1001],{},"Le piège : ",[31,989,743],{}," n'est pas disponible côté serveur (SSR/Nuxt). Utiliser ",[31,992,993],{},"import.meta.client"," ou le wrapper ",[31,996,743],{}," de ",[31,999,1000],{},"@vueuse/nuxt"," qui gère le SSR proprement.",[19,1003,1005,1008],{"id":1004},"useintersectionobserver-lazy-loading-et-animations-au-scroll",[31,1006,1007],{},"useIntersectionObserver"," : lazy loading et animations au scroll",[15,1010,1011],{},"Pour charger des données seulement quand un élément entre dans le viewport :",[24,1013,1015],{"className":64,"code":1014,"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() // Observer une seule fois\n    }\n  },\n  { threshold: 0.1 }, // Déclencher quand 10% de l'élément est visible\n)\n",[31,1016,1017,1028,1040,1044,1071,1088,1092,1110,1115,1130,1147,1154,1163,1174,1179,1183,1197],{"__ignoreMap":29},[34,1018,1019,1021,1024,1026],{"class":36,"line":37},[34,1020,304],{"class":80},[34,1022,1023],{"class":94}," { useIntersectionObserver } ",[34,1025,310],{"class":80},[34,1027,313],{"class":44},[34,1029,1030,1032,1035,1037],{"class":36,"line":77},[34,1031,304],{"class":80},[34,1033,1034],{"class":94}," { ref } ",[34,1036,310],{"class":80},[34,1038,1039],{"class":44}," \"vue\"\n",[34,1041,1042],{"class":36,"line":104},[34,1043,145],{"emptyLinePlaceholder":144},[34,1045,1046,1048,1051,1053,1055,1057,1060,1062,1065,1067,1069],{"class":36,"line":123},[34,1047,81],{"class":80},[34,1049,1050],{"class":84}," target",[34,1052,88],{"class":80},[34,1054,91],{"class":40},[34,1056,946],{"class":94},[34,1058,1059],{"class":40},"HTMLElement",[34,1061,887],{"class":80},[34,1063,1064],{"class":84}," null",[34,1066,952],{"class":94},[34,1068,98],{"class":84},[34,1070,101],{"class":94},[34,1072,1073,1075,1078,1080,1082,1084,1086],{"class":36,"line":141},[34,1074,81],{"class":80},[34,1076,1077],{"class":84}," dataLoaded",[34,1079,88],{"class":80},[34,1081,91],{"class":40},[34,1083,95],{"class":94},[34,1085,118],{"class":84},[34,1087,101],{"class":94},[34,1089,1090],{"class":36,"line":148},[34,1091,145],{"emptyLinePlaceholder":144},[34,1093,1094,1096,1098,1101,1103,1105,1108],{"class":36,"line":170},[34,1095,81],{"class":80},[34,1097,324],{"class":94},[34,1099,1100],{"class":84},"stop",[34,1102,346],{"class":94},[34,1104,176],{"class":80},[34,1106,1107],{"class":40}," useIntersectionObserver",[34,1109,354],{"class":94},[34,1111,1112],{"class":36,"line":182},[34,1113,1114],{"class":94},"  target,\n",[34,1116,1117,1120,1123,1126,1128],{"class":36,"line":193},[34,1118,1119],{"class":94},"  ([{ ",[34,1121,1122],{"class":417},"isIntersecting",[34,1124,1125],{"class":94}," }]) ",[34,1127,164],{"class":80},[34,1129,167],{"class":94},[34,1131,1132,1135,1138,1141,1144],{"class":36,"line":201},[34,1133,1134],{"class":80},"    if",[34,1136,1137],{"class":94}," (isIntersecting ",[34,1139,1140],{"class":80},"&&",[34,1142,1143],{"class":80}," !",[34,1145,1146],{"class":94},"dataLoaded.value) {\n",[34,1148,1149,1152],{"class":36,"line":221},[34,1150,1151],{"class":40},"      loadHeavyData",[34,1153,218],{"class":94},[34,1155,1156,1159,1161],{"class":36,"line":233},[34,1157,1158],{"class":94},"      dataLoaded.value ",[34,1160,176],{"class":80},[34,1162,179],{"class":84},[34,1164,1165,1168,1171],{"class":36,"line":244},[34,1166,1167],{"class":40},"      stop",[34,1169,1170],{"class":94},"() ",[34,1172,1173],{"class":73},"// Observer une seule fois\n",[34,1175,1176],{"class":36,"line":254},[34,1177,1178],{"class":94},"    }\n",[34,1180,1181],{"class":36,"line":265},[34,1182,441],{"class":94},[34,1184,1185,1188,1191,1194],{"class":36,"line":271},[34,1186,1187],{"class":94},"  { threshold: ",[34,1189,1190],{"class":84},"0.1",[34,1192,1193],{"class":94}," }, ",[34,1195,1196],{"class":73},"// Déclencher quand 10% de l'élément est visible\n",[34,1198,1199],{"class":36,"line":277},[34,1200,101],{"class":94},[15,1202,1203],{},"Dans le template :",[24,1205,1209],{"className":1206,"code":1207,"language":1208,"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,1210,1211,1222,1239,1258,1278,1287],{"__ignoreMap":29},[34,1212,1213,1215,1219],{"class":36,"line":37},[34,1214,946],{"class":94},[34,1216,1218],{"class":1217},"sZkSk","template",[34,1220,1221],{"class":94},">\n",[34,1223,1224,1227,1230,1232,1234,1237],{"class":36,"line":77},[34,1225,1226],{"class":94},"  \u003C",[34,1228,1229],{"class":1217},"div",[34,1231,91],{"class":40},[34,1233,176],{"class":94},[34,1235,1236],{"class":44},"\"target\"",[34,1238,1221],{"class":94},[34,1240,1241,1244,1247,1250,1252,1255],{"class":36,"line":104},[34,1242,1243],{"class":94},"    \u003C",[34,1245,1246],{"class":1217},"Spinner",[34,1248,1249],{"class":40}," v-if",[34,1251,176],{"class":94},[34,1253,1254],{"class":44},"\"!dataLoaded\"",[34,1256,1257],{"class":94}," />\n",[34,1259,1260,1262,1265,1268,1271,1273,1276],{"class":36,"line":123},[34,1261,1243],{"class":94},[34,1263,1264],{"class":1217},"HeavyChart",[34,1266,1267],{"class":40}," v-else",[34,1269,1270],{"class":40}," :data",[34,1272,176],{"class":94},[34,1274,1275],{"class":44},"\"chartData\"",[34,1277,1257],{"class":94},[34,1279,1280,1283,1285],{"class":36,"line":141},[34,1281,1282],{"class":94},"  \u003C/",[34,1284,1229],{"class":1217},[34,1286,1221],{"class":94},[34,1288,1289,1292,1294],{"class":36,"line":148},[34,1290,1291],{"class":94},"\u003C/",[34,1293,1218],{"class":1217},[34,1295,1221],{"class":94},[15,1297,1298,1301],{},[31,1299,1300],{},"stop()"," arrête l'observation après le premier déclenchement — évite des appels répétés inutiles. Utile aussi pour les animations d'entrée : déclencher une classe CSS quand l'élément devient visible.",[19,1303,1305,1308],{"id":1304},"useeventlistener-gestion-propre-des-événements-dom",[31,1306,1307],{},"useEventListener"," : gestion propre des événements DOM",[24,1310,1312],{"className":64,"code":1311,"language":66,"meta":29,"style":29},"import { useEventListener } from \"@vueuse/core\"\n\n// Nettoyage automatique au démontage du composant\nuseEventListener(window, \"keydown\", (event: KeyboardEvent) => {\n  if (event.key === \"Escape\") closeModal()\n  if (event.ctrlKey && event.key === \"s\") saveForm()\n})\n\n// Sur un élément réactif\nconst tableRef = ref\u003CHTMLElement | null>(null)\nuseEventListener(tableRef, \"click\", handleCellClick)\n",[31,1313,1314,1325,1329,1334,1360,1380,1404,1408,1412,1417,1442],{"__ignoreMap":29},[34,1315,1316,1318,1321,1323],{"class":36,"line":37},[34,1317,304],{"class":80},[34,1319,1320],{"class":94}," { useEventListener } ",[34,1322,310],{"class":80},[34,1324,313],{"class":44},[34,1326,1327],{"class":36,"line":77},[34,1328,145],{"emptyLinePlaceholder":144},[34,1330,1331],{"class":36,"line":104},[34,1332,1333],{"class":73},"// Nettoyage automatique au démontage du composant\n",[34,1335,1336,1338,1341,1344,1347,1349,1351,1354,1356,1358],{"class":36,"line":123},[34,1337,1307],{"class":40},[34,1339,1340],{"class":94},"(window, ",[34,1342,1343],{"class":44},"\"keydown\"",[34,1345,1346],{"class":94},", (",[34,1348,703],{"class":417},[34,1350,582],{"class":80},[34,1352,1353],{"class":40}," KeyboardEvent",[34,1355,421],{"class":94},[34,1357,164],{"class":80},[34,1359,167],{"class":94},[34,1361,1362,1364,1367,1370,1373,1375,1378],{"class":36,"line":141},[34,1363,596],{"class":80},[34,1365,1366],{"class":94}," (event.key ",[34,1368,1369],{"class":80},"===",[34,1371,1372],{"class":44}," \"Escape\"",[34,1374,421],{"class":94},[34,1376,1377],{"class":40},"closeModal",[34,1379,218],{"class":94},[34,1381,1382,1384,1387,1389,1392,1394,1397,1399,1402],{"class":36,"line":148},[34,1383,596],{"class":80},[34,1385,1386],{"class":94}," (event.ctrlKey ",[34,1388,1140],{"class":80},[34,1390,1391],{"class":94}," event.key ",[34,1393,1369],{"class":80},[34,1395,1396],{"class":44}," \"s\"",[34,1398,421],{"class":94},[34,1400,1401],{"class":40},"saveForm",[34,1403,218],{"class":94},[34,1405,1406],{"class":36,"line":170},[34,1407,825],{"class":94},[34,1409,1410],{"class":36,"line":182},[34,1411,145],{"emptyLinePlaceholder":144},[34,1413,1414],{"class":36,"line":193},[34,1415,1416],{"class":73},"// Sur un élément réactif\n",[34,1418,1419,1421,1424,1426,1428,1430,1432,1434,1436,1438,1440],{"class":36,"line":201},[34,1420,81],{"class":80},[34,1422,1423],{"class":84}," tableRef",[34,1425,88],{"class":80},[34,1427,91],{"class":40},[34,1429,946],{"class":94},[34,1431,1059],{"class":40},[34,1433,887],{"class":80},[34,1435,1064],{"class":84},[34,1437,952],{"class":94},[34,1439,98],{"class":84},[34,1441,101],{"class":94},[34,1443,1444,1446,1449,1452],{"class":36,"line":221},[34,1445,1307],{"class":40},[34,1447,1448],{"class":94},"(tableRef, ",[34,1450,1451],{"class":44},"\"click\"",[34,1453,1454],{"class":94},", handleCellClick)\n",[15,1456,1457,1458,1461,1462,1465,1466,1468],{},"Sans VueUse, il faut penser à ",[31,1459,1460],{},"removeEventListener"," dans ",[31,1463,1464],{},"onUnmounted"," — facile à oublier, source de fuites mémoire. ",[31,1467,1307],{}," le fait automatiquement.",[19,1470,1472,1475],{"id":1471},"useclipboard-copier-dans-le-presse-papiers",[31,1473,1474],{},"useClipboard"," : copier dans le presse-papiers",[24,1477,1479],{"className":64,"code":1478,"language":66,"meta":29,"style":29},"import { useClipboard } from \"@vueuse/core\"\n\nconst { copy, copied, isSupported } = useClipboard()\n\n// Dans le template\n",[31,1480,1481,1492,1496,1524,1528],{"__ignoreMap":29},[34,1482,1483,1485,1488,1490],{"class":36,"line":37},[34,1484,304],{"class":80},[34,1486,1487],{"class":94}," { useClipboard } ",[34,1489,310],{"class":80},[34,1491,313],{"class":44},[34,1493,1494],{"class":36,"line":77},[34,1495,145],{"emptyLinePlaceholder":144},[34,1497,1498,1500,1502,1505,1507,1510,1512,1515,1517,1519,1522],{"class":36,"line":104},[34,1499,81],{"class":80},[34,1501,324],{"class":94},[34,1503,1504],{"class":84},"copy",[34,1506,330],{"class":94},[34,1508,1509],{"class":84},"copied",[34,1511,330],{"class":94},[34,1513,1514],{"class":84},"isSupported",[34,1516,346],{"class":94},[34,1518,176],{"class":80},[34,1520,1521],{"class":40}," useClipboard",[34,1523,218],{"class":94},[34,1525,1526],{"class":36,"line":123},[34,1527,145],{"emptyLinePlaceholder":144},[34,1529,1530],{"class":36,"line":141},[34,1531,652],{"class":73},[24,1533,1535],{"className":1206,"code":1534,"language":1208,"meta":29,"style":29},"\u003Ctemplate>\n  \u003Cbutton @click=\"copy(certificateId)\" :disabled=\"!isSupported\">\n    {{ copied ? \"✓ Copié\" : \"Copier l'ID\" }}\n  \u003C/button>\n\u003C/template>\n",[31,1536,1537,1545,1570,1575,1583],{"__ignoreMap":29},[34,1538,1539,1541,1543],{"class":36,"line":37},[34,1540,946],{"class":94},[34,1542,1218],{"class":1217},[34,1544,1221],{"class":94},[34,1546,1547,1549,1552,1555,1557,1560,1563,1565,1568],{"class":36,"line":77},[34,1548,1226],{"class":94},[34,1550,1551],{"class":1217},"button",[34,1553,1554],{"class":40}," @click",[34,1556,176],{"class":94},[34,1558,1559],{"class":44},"\"copy(certificateId)\"",[34,1561,1562],{"class":40}," :disabled",[34,1564,176],{"class":94},[34,1566,1567],{"class":44},"\"!isSupported\"",[34,1569,1221],{"class":94},[34,1571,1572],{"class":36,"line":104},[34,1573,1574],{"class":94},"    {{ copied ? \"✓ Copié\" : \"Copier l'ID\" }}\n",[34,1576,1577,1579,1581],{"class":36,"line":123},[34,1578,1282],{"class":94},[34,1580,1551],{"class":1217},[34,1582,1221],{"class":94},[34,1584,1585,1587,1589],{"class":36,"line":141},[34,1586,1291],{"class":94},[34,1588,1218],{"class":1217},[34,1590,1221],{"class":94},[15,1592,1593,1595,1596,1598,1599,1601],{},[31,1594,1509],{}," revient automatiquement à ",[31,1597,118],{}," après 1.5s (configurable). ",[31,1600,1514],{}," vérifie si l'API Clipboard est disponible dans le navigateur — utile pour les fallbacks.",[19,1603,1605,1608],{"id":1604},"usemediaquery-responsive-sans-css",[31,1606,1607],{},"useMediaQuery"," : responsive sans CSS",[24,1610,1612],{"className":64,"code":1611,"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// Réactif — se met à jour quand la fenêtre est redimensionnée\nwatch(isMobile, (mobile) => {\n  if (mobile) collapseNavigation()\n})\n",[31,1613,1614,1625,1629,1648,1666,1684,1688,1693,1709,1721],{"__ignoreMap":29},[34,1615,1616,1618,1621,1623],{"class":36,"line":37},[34,1617,304],{"class":80},[34,1619,1620],{"class":94}," { useMediaQuery } ",[34,1622,310],{"class":80},[34,1624,313],{"class":44},[34,1626,1627],{"class":36,"line":77},[34,1628,145],{"emptyLinePlaceholder":144},[34,1630,1631,1633,1636,1638,1641,1643,1646],{"class":36,"line":104},[34,1632,81],{"class":80},[34,1634,1635],{"class":84}," isMobile",[34,1637,88],{"class":80},[34,1639,1640],{"class":40}," useMediaQuery",[34,1642,95],{"class":94},[34,1644,1645],{"class":44},"\"(max-width: 768px)\"",[34,1647,101],{"class":94},[34,1649,1650,1652,1655,1657,1659,1661,1664],{"class":36,"line":123},[34,1651,81],{"class":80},[34,1653,1654],{"class":84}," prefersReducedMotion",[34,1656,88],{"class":80},[34,1658,1640],{"class":40},[34,1660,95],{"class":94},[34,1662,1663],{"class":44},"\"(prefers-reduced-motion: reduce)\"",[34,1665,101],{"class":94},[34,1667,1668,1670,1673,1675,1677,1679,1682],{"class":36,"line":141},[34,1669,81],{"class":80},[34,1671,1672],{"class":84}," isDarkMode",[34,1674,88],{"class":80},[34,1676,1640],{"class":40},[34,1678,95],{"class":94},[34,1680,1681],{"class":44},"\"(prefers-color-scheme: dark)\"",[34,1683,101],{"class":94},[34,1685,1686],{"class":36,"line":148},[34,1687,145],{"emptyLinePlaceholder":144},[34,1689,1690],{"class":36,"line":170},[34,1691,1692],{"class":73},"// Réactif — se met à jour quand la fenêtre est redimensionnée\n",[34,1694,1695,1697,1700,1703,1705,1707],{"class":36,"line":182},[34,1696,657],{"class":40},[34,1698,1699],{"class":94},"(isMobile, (",[34,1701,1702],{"class":417},"mobile",[34,1704,421],{"class":94},[34,1706,164],{"class":80},[34,1708,167],{"class":94},[34,1710,1711,1713,1716,1719],{"class":36,"line":193},[34,1712,596],{"class":80},[34,1714,1715],{"class":94}," (mobile) ",[34,1717,1718],{"class":40},"collapseNavigation",[34,1720,218],{"class":94},[34,1722,1723],{"class":36,"line":201},[34,1724,825],{"class":94},[15,1726,1727],{},"Utile quand la logique JavaScript doit changer selon la taille d'écran — pas seulement le CSS. Par exemple, désactiver des animations complexes sur mobile ou réduire la quantité de données chargées.",[19,1729,1731,1734],{"id":1730},"useeventsource-consommer-un-flux-sse",[31,1732,1733],{},"useEventSource"," : consommer un flux SSE",[15,1736,1737],{},"Server-Sent Events est souvent préférable aux WebSockets pour les flux unidirectionnels (notifications, mises à jour de statut) — plus simple, reconnexion automatique native, compatible avec les proxies HTTP.",[24,1739,1741],{"className":64,"code":1740,"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\"], // Événements à écouter\n  { withCredentials: true },\n)\n\n// data est la dernière donnée reçue\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,1742,1743,1754,1758,1790,1797,1816,1826,1830,1834,1839,1855,1869,1891,1899,1903,1907],{"__ignoreMap":29},[34,1744,1745,1747,1750,1752],{"class":36,"line":37},[34,1746,304],{"class":80},[34,1748,1749],{"class":94}," { useEventSource } ",[34,1751,310],{"class":80},[34,1753,313],{"class":44},[34,1755,1756],{"class":36,"line":77},[34,1757,145],{"emptyLinePlaceholder":144},[34,1759,1760,1762,1764,1767,1769,1772,1774,1776,1778,1781,1783,1785,1788],{"class":36,"line":104},[34,1761,81],{"class":80},[34,1763,324],{"class":94},[34,1765,1766],{"class":84},"data",[34,1768,330],{"class":94},[34,1770,1771],{"class":84},"status",[34,1773,330],{"class":94},[34,1775,338],{"class":84},[34,1777,330],{"class":94},[34,1779,1780],{"class":84},"close",[34,1782,346],{"class":94},[34,1784,176],{"class":80},[34,1786,1787],{"class":40}," useEventSource",[34,1789,354],{"class":94},[34,1791,1792,1795],{"class":36,"line":123},[34,1793,1794],{"class":44},"  \"/api/events/certificates\"",[34,1796,802],{"class":94},[34,1798,1799,1802,1805,1807,1810,1813],{"class":36,"line":141},[34,1800,1801],{"class":94},"  [",[34,1803,1804],{"class":44},"\"certificate_updated\"",[34,1806,330],{"class":94},[34,1808,1809],{"class":44},"\"certificate_created\"",[34,1811,1812],{"class":94},"], ",[34,1814,1815],{"class":73},"// Événements à écouter\n",[34,1817,1818,1821,1823],{"class":36,"line":148},[34,1819,1820],{"class":94},"  { withCredentials: ",[34,1822,389],{"class":84},[34,1824,1825],{"class":94}," },\n",[34,1827,1828],{"class":36,"line":170},[34,1829,101],{"class":94},[34,1831,1832],{"class":36,"line":182},[34,1833,145],{"emptyLinePlaceholder":144},[34,1835,1836],{"class":36,"line":193},[34,1837,1838],{"class":73},"// data est la dernière donnée reçue\n",[34,1840,1841,1843,1846,1849,1851,1853],{"class":36,"line":201},[34,1842,657],{"class":40},[34,1844,1845],{"class":94},"(data, (",[34,1847,1848],{"class":417},"raw",[34,1850,421],{"class":94},[34,1852,164],{"class":80},[34,1854,167],{"class":94},[34,1856,1857,1859,1861,1864,1867],{"class":36,"line":221},[34,1858,596],{"class":80},[34,1860,576],{"class":94},[34,1862,1863],{"class":80},"!",[34,1865,1866],{"class":94},"raw) ",[34,1868,613],{"class":80},[34,1870,1871,1874,1877,1879,1882,1885,1888],{"class":36,"line":233},[34,1872,1873],{"class":80},"  const",[34,1875,1876],{"class":84}," event",[34,1878,88],{"class":80},[34,1880,1881],{"class":84}," JSON",[34,1883,1884],{"class":94},".",[34,1886,1887],{"class":40},"parse",[34,1889,1890],{"class":94},"(raw)\n",[34,1892,1893,1896],{"class":36,"line":244},[34,1894,1895],{"class":40},"  updateCertificateInList",[34,1897,1898],{"class":94},"(event)\n",[34,1900,1901],{"class":36,"line":254},[34,1902,825],{"class":94},[34,1904,1905],{"class":36,"line":265},[34,1906,145],{"emptyLinePlaceholder":144},[34,1908,1909],{"class":36,"line":271},[34,1910,1911],{"class":73},"// status : 'CONNECTING' | 'OPEN' | 'CLOSED'\n",[15,1913,1914],{},"Côté FastAPI, un endpoint SSE minimal :",[24,1916,1920],{"className":1917,"code":1918,"language":1919,"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,1921,1922,1927,1932,1937,1941,1946,1951,1956,1961,1966,1971,1976,1981,1985,1990,1995,2000,2005],{"__ignoreMap":29},[34,1923,1924],{"class":36,"line":37},[34,1925,1926],{},"from fastapi.responses import StreamingResponse\n",[34,1928,1929],{"class":36,"line":77},[34,1930,1931],{},"import asyncio\n",[34,1933,1934],{"class":36,"line":104},[34,1935,1936],{},"import json\n",[34,1938,1939],{"class":36,"line":123},[34,1940,145],{"emptyLinePlaceholder":144},[34,1942,1943],{"class":36,"line":141},[34,1944,1945],{},"@router.get(\"/api/events/certificates\")\n",[34,1947,1948],{"class":36,"line":148},[34,1949,1950],{},"async def certificate_events(request: Request):\n",[34,1952,1953],{"class":36,"line":170},[34,1954,1955],{},"    async def event_generator():\n",[34,1957,1958],{"class":36,"line":182},[34,1959,1960],{},"        while True:\n",[34,1962,1963],{"class":36,"line":193},[34,1964,1965],{},"            if await request.is_disconnected():\n",[34,1967,1968],{"class":36,"line":201},[34,1969,1970],{},"                break\n",[34,1972,1973],{"class":36,"line":221},[34,1974,1975],{},"            event = await event_queue.get()\n",[34,1977,1978],{"class":36,"line":233},[34,1979,1980],{},"            yield f\"event: {event['type']}\\ndata: {json.dumps(event)}\\n\\n\"\n",[34,1982,1983],{"class":36,"line":244},[34,1984,145],{"emptyLinePlaceholder":144},[34,1986,1987],{"class":36,"line":254},[34,1988,1989],{},"    return StreamingResponse(\n",[34,1991,1992],{"class":36,"line":265},[34,1993,1994],{},"        event_generator(),\n",[34,1996,1997],{"class":36,"line":271},[34,1998,1999],{},"        media_type=\"text/event-stream\",\n",[34,2001,2002],{"class":36,"line":277},[34,2003,2004],{},"        headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\"}\n",[34,2006,2007],{"class":36,"line":282},[34,2008,2009],{},"    )\n",[15,2011,2012,2015],{},[31,2013,2014],{},"X-Accel-Buffering: no"," est critique derrière nginx ou un ingress OpenShift — sans ça, les événements sont bufferisés et n'arrivent pas en temps réel.",[19,2017,2019,2022],{"id":2018},"usevmodel-simplifier-les-composants-formulaire",[31,2020,2021],{},"useVModel"," : simplifier les composants formulaire",[15,2024,2025,2026,294],{},"Pour un composant qui wrape un input et doit supporter ",[31,2027,2028],{},"v-model",[24,2030,2032],{"className":64,"code":2031,"language":66,"meta":29,"style":29},"import { useVModel } from \"@vueuse/core\"\n\n// Composant 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 est une Ref writable — directement utilisable dans le template\n",[31,2033,2034,2045,2049,2054,2069,2079,2088,2093,2114,2118,2139,2143],{"__ignoreMap":29},[34,2035,2036,2038,2041,2043],{"class":36,"line":37},[34,2037,304],{"class":80},[34,2039,2040],{"class":94}," { useVModel } ",[34,2042,310],{"class":80},[34,2044,313],{"class":44},[34,2046,2047],{"class":36,"line":77},[34,2048,145],{"emptyLinePlaceholder":144},[34,2050,2051],{"class":36,"line":104},[34,2052,2053],{"class":73},"// Composant InputField.vue\n",[34,2055,2056,2058,2061,2063,2066],{"class":36,"line":123},[34,2057,81],{"class":80},[34,2059,2060],{"class":84}," props",[34,2062,88],{"class":80},[34,2064,2065],{"class":40}," defineProps",[34,2067,2068],{"class":94},"\u003C{\n",[34,2070,2071,2074,2076],{"class":36,"line":141},[34,2072,2073],{"class":417},"  modelValue",[34,2075,582],{"class":80},[34,2077,2078],{"class":84}," string\n",[34,2080,2081,2084,2086],{"class":36,"line":148},[34,2082,2083],{"class":417},"  label",[34,2085,582],{"class":80},[34,2087,2078],{"class":84},[34,2089,2090],{"class":36,"line":170},[34,2091,2092],{"class":94},"}>()\n",[34,2094,2095,2097,2100,2102,2105,2108,2111],{"class":36,"line":182},[34,2096,81],{"class":80},[34,2098,2099],{"class":84}," emit",[34,2101,88],{"class":80},[34,2103,2104],{"class":40}," defineEmits",[34,2106,2107],{"class":94},"([",[34,2109,2110],{"class":44},"\"update:modelValue\"",[34,2112,2113],{"class":94},"])\n",[34,2115,2116],{"class":36,"line":193},[34,2117,145],{"emptyLinePlaceholder":144},[34,2119,2120,2122,2125,2127,2130,2133,2136],{"class":36,"line":201},[34,2121,81],{"class":80},[34,2123,2124],{"class":84}," value",[34,2126,88],{"class":80},[34,2128,2129],{"class":40}," useVModel",[34,2131,2132],{"class":94},"(props, ",[34,2134,2135],{"class":44},"\"modelValue\"",[34,2137,2138],{"class":94},", emit)\n",[34,2140,2141],{"class":36,"line":221},[34,2142,145],{"emptyLinePlaceholder":144},[34,2144,2145],{"class":36,"line":233},[34,2146,2147],{"class":73},"// value est une Ref writable — directement utilisable dans le template\n",[24,2149,2151],{"className":1206,"code":2150,"language":1208,"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,2152,2153,2161,2169,2183,2200,2208],{"__ignoreMap":29},[34,2154,2155,2157,2159],{"class":36,"line":37},[34,2156,946],{"class":94},[34,2158,1218],{"class":1217},[34,2160,1221],{"class":94},[34,2162,2163,2165,2167],{"class":36,"line":77},[34,2164,1226],{"class":94},[34,2166,1229],{"class":1217},[34,2168,1221],{"class":94},[34,2170,2171,2173,2176,2179,2181],{"class":36,"line":104},[34,2172,1243],{"class":94},[34,2174,2175],{"class":1217},"label",[34,2177,2178],{"class":94},">{{ label }}\u003C/",[34,2180,2175],{"class":1217},[34,2182,1221],{"class":94},[34,2184,2185,2187,2190,2193,2195,2198],{"class":36,"line":123},[34,2186,1243],{"class":94},[34,2188,2189],{"class":1217},"input",[34,2191,2192],{"class":40}," v-model",[34,2194,176],{"class":94},[34,2196,2197],{"class":44},"\"value\"",[34,2199,1257],{"class":94},[34,2201,2202,2204,2206],{"class":36,"line":141},[34,2203,1282],{"class":94},[34,2205,1229],{"class":1217},[34,2207,1221],{"class":94},[34,2209,2210,2212,2214],{"class":36,"line":148},[34,2211,1291],{"class":94},[34,2213,1218],{"class":1217},[34,2215,1221],{"class":94},[15,2217,2218,2219,2221],{},"Sans ",[31,2220,2021],{},", il faut gérer manuellement la prop et l'emit — deux lignes de plus, et le risque de mutation directe de la prop.",[19,2223,2225],{"id":2224},"ce-quil-faut-retenir","Ce qu'il faut retenir",[15,2227,2228,2229,330,2231,2233,2234,330,2236,330,2238,2240,2241,330,2243,2245],{},"VueUse vaut surtout pour trois catégories de composables : ceux qui éliminent du boilerplate récurrent (",[31,2230,57],{},[31,2232,2021],{},"), ceux qui wrappent des APIs navigateur verbeuses (",[31,2235,1007],{},[31,2237,1307],{},[31,2239,1474],{},"), et ceux qui gèrent des problèmes de performance (",[31,2242,506],{},[31,2244,510],{},"). Le reste est utile situationnellement — mais ces dix-là reviennent sur pratiquement tous les projets Vue.js professionnels.",[2247,2248,2249],"style",{},"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 .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 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":2251},[2252,2253,2255,2257,2259,2261,2263,2265,2267,2269,2271],{"id":21,"depth":77,"text":22},{"id":54,"depth":77,"text":2254},"useAsyncState : remplacer le pattern loading/error/data",{"id":503,"depth":77,"text":2256},"useDebounceFn et useThrottleFn : performances sur les événements fréquents",{"id":740,"depth":77,"text":2258},"useLocalStorage et useSessionStorage : état persistant réactif",{"id":1004,"depth":77,"text":2260},"useIntersectionObserver : lazy loading et animations au scroll",{"id":1304,"depth":77,"text":2262},"useEventListener : gestion propre des événements DOM",{"id":1471,"depth":77,"text":2264},"useClipboard : copier dans le presse-papiers",{"id":1604,"depth":77,"text":2266},"useMediaQuery : responsive sans CSS",{"id":1730,"depth":77,"text":2268},"useEventSource : consommer un flux SSE",{"id":2018,"depth":77,"text":2270},"useVModel : simplifier les composants formulaire",{"id":2224,"depth":77,"text":2225},"2025-01-13",null,"md",{},"/fr/blog/vueuse-essentiels",{"title":5,"description":17},"vueuse-essentiels","fr/blog/vueuse-essentiels",[2281,2282,2283,2284],"Vue.js","VueUse","Composition API","Frontend","NnfEIT0KyExt-UaTectZEEjsrCvGlwI1zGUmTUc910k",1774645635864]