[{"data":1,"prerenderedAt":1681},["ShallowReactive",2],{"content-\u002Fdocs\u002Fcross-cutting-state\u002Fautosave":3},{"id":4,"title":5,"body":6,"description":1660,"extension":1661,"meta":1662,"metaRows":1663,"navigation":117,"path":1676,"seo":1677,"source":1678,"stem":1679,"__hash__":1680},"docs\u002Fdocs\u002Fcross-cutting-state\u002Fautosave.md","Autosave",{"type":7,"value":8,"toc":1651},"minimark",[9,13,33,36,39,43,48,60,1101,1124,1128,1211,1227,1231,1248,1470,1482,1486,1501,1514,1528,1532,1543,1587,1591,1605,1609,1647],[10,11,5],"h1",{"id":12},"autosave",[14,15,16],"blockquote",{},[17,18,19,20,24,25,28,29,32],"p",{},"A copy-paste ",[21,22,23],"code",{},"useAutosave"," composable built on ",[21,26,27],{},"form.onChange",": per-field status, an aggregate ",[21,30,31],{},"isSaving",", a validity gate, and debounced writes. Attaform ships the primitive, you own the policy.",[34,35],"docs-meta-table",{},[17,37,38],{},"Email saves as a draft: even an invalid address persists as you type, while Attaform still flags it inline below the field. Display name and bio gate on validity instead, holding back until they pass (display name needs two characters). Type a burst and one save lands per pause, the toast confirming it. Tick the box to fail saves and watch the aggregate banner flip.",[40,41],"docs-demo",{"label":42,"slug":12},"Autosave Recipe Demo",[44,45,47],"h2",{"id":46},"the-recipe","The recipe",[17,49,50,52,53,59],{},[21,51,23],{}," wraps ",[54,55,57],"a",{"href":56},"\u002Fdocs\u002Fcross-cutting-state\u002Fon-change",[21,58,27],{}," per path: it debounces each field, gates the save on the field being valid, tracks per-path status, and rolls that up into an aggregate. Drop it into your app as a composable and own it from there.",[61,62,67],"pre",{"className":63,"code":64,"language":65,"meta":66,"style":66},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F composables\u002FuseAutosave.ts\nimport { computed, reactive } from 'vue'\nimport type { FlatPath, GenericForm, UseFormReturnType } from 'attaform'\n\ntype SaveStatus = 'idle' | 'pending' | 'saving' | 'saved' | 'error'\n\nfunction debounce\u003CA extends unknown[]>(fn: (...args: A) => void, ms: number) {\n  let timer: ReturnType\u003Ctypeof setTimeout> | undefined\n  return (...args: A) => {\n    if (timer) clearTimeout(timer)\n    timer = setTimeout(() => fn(...args), ms)\n  }\n}\n\nexport function useAutosave\u003CForm extends GenericForm>(\n  form: UseFormReturnType\u003CForm>,\n  paths: readonly FlatPath\u003CForm>[],\n  save: (path: FlatPath\u003CForm>, value: unknown, signal: AbortSignal) => Promise\u003Cvoid>,\n  options: {\n    debounceMs?: number\n    gateOnValidity?: boolean | ((path: FlatPath\u003CForm>) => boolean)\n  } = {}\n) {\n  const { debounceMs = 600, gateOnValidity = true } = options\n  const status = reactive\u003CRecord\u003Cstring, SaveStatus>>({})\n  for (const path of paths) status[path] = 'idle'\n\n  async function run(path: FlatPath\u003CForm>, value: unknown, signal: AbortSignal) {\n    try {\n      const gate = typeof gateOnValidity === 'function' ? gateOnValidity(path) : gateOnValidity\n      if (gate && !(await form.validateAsync(path)).success) {\n        status[path] = 'idle'\n        return\n      }\n      if (signal.aborted) return\n      status[path] = 'saving'\n      await save(path, value, signal)\n      if (signal.aborted) return\n      status[path] = 'saved'\n    } catch {\n      if (!signal.aborted) status[path] = 'error'\n    }\n  }\n\n  for (const path of paths) {\n    const schedule = debounce(\n      (value: unknown, signal: AbortSignal) => run(path, value, signal),\n      debounceMs\n    )\n    form.onChange(path, (value, ctx) => {\n      status[path] = 'pending'\n      schedule(value, ctx.signal)\n    })\n  }\n\n  return {\n    status,\n    isSaving: computed(() => Object.values(status).some((s) => s === 'saving')),\n    failed: computed(() => paths.filter((path) => status[path] === 'error')),\n  }\n}\n","ts","",[21,68,69,78,96,112,119,156,161,230,258,281,296,324,330,336,341,366,384,405,460,470,482,518,529,534,569,600,625,630,671,679,716,745,755,761,767,778,789,801,810,820,831,848,854,859,864,880,896,925,931,937,962,972,981,987,992,997,1004,1010,1055,1091,1096],{"__ignoreMap":66},[70,71,74],"span",{"class":72,"line":73},"line",1,[70,75,77],{"class":76},"sJ8bj","\u002F\u002F composables\u002FuseAutosave.ts\n",[70,79,81,85,89,92],{"class":72,"line":80},2,[70,82,84],{"class":83},"szBVR","import",[70,86,88],{"class":87},"sVt8B"," { computed, reactive } ",[70,90,91],{"class":83},"from",[70,93,95],{"class":94},"sZZnC"," 'vue'\n",[70,97,99,101,104,107,109],{"class":72,"line":98},3,[70,100,84],{"class":83},[70,102,103],{"class":83}," type",[70,105,106],{"class":87}," { FlatPath, GenericForm, UseFormReturnType } ",[70,108,91],{"class":83},[70,110,111],{"class":94}," 'attaform'\n",[70,113,115],{"class":72,"line":114},4,[70,116,118],{"emptyLinePlaceholder":117},true,"\n",[70,120,122,125,129,132,135,138,141,143,146,148,151,153],{"class":72,"line":121},5,[70,123,124],{"class":83},"type",[70,126,128],{"class":127},"sScJk"," SaveStatus",[70,130,131],{"class":83}," =",[70,133,134],{"class":94}," 'idle'",[70,136,137],{"class":83}," |",[70,139,140],{"class":94}," 'pending'",[70,142,137],{"class":83},[70,144,145],{"class":94}," 'saving'",[70,147,137],{"class":83},[70,149,150],{"class":94}," 'saved'",[70,152,137],{"class":83},[70,154,155],{"class":94}," 'error'\n",[70,157,159],{"class":72,"line":158},6,[70,160,118],{"emptyLinePlaceholder":117},[70,162,164,167,170,173,176,179,183,186,189,192,195,198,202,204,207,210,213,216,219,222,224,227],{"class":72,"line":163},7,[70,165,166],{"class":83},"function",[70,168,169],{"class":127}," debounce",[70,171,172],{"class":87},"\u003C",[70,174,175],{"class":127},"A",[70,177,178],{"class":83}," extends",[70,180,182],{"class":181},"sj4cs"," unknown",[70,184,185],{"class":87},"[]>(",[70,187,188],{"class":127},"fn",[70,190,191],{"class":83},":",[70,193,194],{"class":87}," (",[70,196,197],{"class":83},"...",[70,199,201],{"class":200},"s4XuR","args",[70,203,191],{"class":83},[70,205,206],{"class":127}," A",[70,208,209],{"class":87},") ",[70,211,212],{"class":83},"=>",[70,214,215],{"class":181}," void",[70,217,218],{"class":87},", ",[70,220,221],{"class":200},"ms",[70,223,191],{"class":83},[70,225,226],{"class":181}," number",[70,228,229],{"class":87},") {\n",[70,231,233,236,239,241,244,246,249,252,255],{"class":72,"line":232},8,[70,234,235],{"class":83},"  let",[70,237,238],{"class":87}," timer",[70,240,191],{"class":83},[70,242,243],{"class":127}," ReturnType",[70,245,172],{"class":87},[70,247,248],{"class":83},"typeof",[70,250,251],{"class":87}," setTimeout> ",[70,253,254],{"class":83},"|",[70,256,257],{"class":181}," undefined\n",[70,259,261,264,266,268,270,272,274,276,278],{"class":72,"line":260},9,[70,262,263],{"class":83},"  return",[70,265,194],{"class":87},[70,267,197],{"class":83},[70,269,201],{"class":200},[70,271,191],{"class":83},[70,273,206],{"class":127},[70,275,209],{"class":87},[70,277,212],{"class":83},[70,279,280],{"class":87}," {\n",[70,282,284,287,290,293],{"class":72,"line":283},10,[70,285,286],{"class":83},"    if",[70,288,289],{"class":87}," (timer) ",[70,291,292],{"class":127},"clearTimeout",[70,294,295],{"class":87},"(timer)\n",[70,297,299,302,305,308,311,313,316,319,321],{"class":72,"line":298},11,[70,300,301],{"class":87},"    timer ",[70,303,304],{"class":83},"=",[70,306,307],{"class":127}," setTimeout",[70,309,310],{"class":87},"(() ",[70,312,212],{"class":83},[70,314,315],{"class":127}," fn",[70,317,318],{"class":87},"(",[70,320,197],{"class":83},[70,322,323],{"class":87},"args), ms)\n",[70,325,327],{"class":72,"line":326},12,[70,328,329],{"class":87},"  }\n",[70,331,333],{"class":72,"line":332},13,[70,334,335],{"class":87},"}\n",[70,337,339],{"class":72,"line":338},14,[70,340,118],{"emptyLinePlaceholder":117},[70,342,344,347,350,353,355,358,360,363],{"class":72,"line":343},15,[70,345,346],{"class":83},"export",[70,348,349],{"class":83}," function",[70,351,352],{"class":127}," useAutosave",[70,354,172],{"class":87},[70,356,357],{"class":127},"Form",[70,359,178],{"class":83},[70,361,362],{"class":127}," GenericForm",[70,364,365],{"class":87},">(\n",[70,367,369,372,374,377,379,381],{"class":72,"line":368},16,[70,370,371],{"class":200},"  form",[70,373,191],{"class":83},[70,375,376],{"class":127}," UseFormReturnType",[70,378,172],{"class":87},[70,380,357],{"class":127},[70,382,383],{"class":87},">,\n",[70,385,387,390,392,395,398,400,402],{"class":72,"line":386},17,[70,388,389],{"class":200},"  paths",[70,391,191],{"class":83},[70,393,394],{"class":83}," readonly",[70,396,397],{"class":127}," FlatPath",[70,399,172],{"class":87},[70,401,357],{"class":127},[70,403,404],{"class":87},">[],\n",[70,406,408,411,413,415,418,420,422,424,426,429,432,434,436,438,441,443,446,448,450,453,455,458],{"class":72,"line":407},18,[70,409,410],{"class":127},"  save",[70,412,191],{"class":83},[70,414,194],{"class":87},[70,416,417],{"class":200},"path",[70,419,191],{"class":83},[70,421,397],{"class":127},[70,423,172],{"class":87},[70,425,357],{"class":127},[70,427,428],{"class":87},">, ",[70,430,431],{"class":200},"value",[70,433,191],{"class":83},[70,435,182],{"class":181},[70,437,218],{"class":87},[70,439,440],{"class":200},"signal",[70,442,191],{"class":83},[70,444,445],{"class":127}," AbortSignal",[70,447,209],{"class":87},[70,449,212],{"class":83},[70,451,452],{"class":127}," Promise",[70,454,172],{"class":87},[70,456,457],{"class":181},"void",[70,459,383],{"class":87},[70,461,463,466,468],{"class":72,"line":462},19,[70,464,465],{"class":200},"  options",[70,467,191],{"class":83},[70,469,280],{"class":87},[70,471,473,476,479],{"class":72,"line":472},20,[70,474,475],{"class":200},"    debounceMs",[70,477,478],{"class":83},"?:",[70,480,481],{"class":181}," number\n",[70,483,485,488,490,493,495,498,500,502,504,506,508,511,513,515],{"class":72,"line":484},21,[70,486,487],{"class":200},"    gateOnValidity",[70,489,478],{"class":83},[70,491,492],{"class":181}," boolean",[70,494,137],{"class":83},[70,496,497],{"class":87}," ((",[70,499,417],{"class":200},[70,501,191],{"class":83},[70,503,397],{"class":127},[70,505,172],{"class":87},[70,507,357],{"class":127},[70,509,510],{"class":87},">) ",[70,512,212],{"class":83},[70,514,492],{"class":181},[70,516,517],{"class":87},")\n",[70,519,521,524,526],{"class":72,"line":520},22,[70,522,523],{"class":87},"  } ",[70,525,304],{"class":83},[70,527,528],{"class":87}," {}\n",[70,530,532],{"class":72,"line":531},23,[70,533,229],{"class":87},[70,535,537,540,543,546,548,551,553,556,558,561,564,566],{"class":72,"line":536},24,[70,538,539],{"class":83},"  const",[70,541,542],{"class":87}," { ",[70,544,545],{"class":181},"debounceMs",[70,547,131],{"class":83},[70,549,550],{"class":181}," 600",[70,552,218],{"class":87},[70,554,555],{"class":181},"gateOnValidity",[70,557,131],{"class":83},[70,559,560],{"class":181}," true",[70,562,563],{"class":87}," } ",[70,565,304],{"class":83},[70,567,568],{"class":87}," options\n",[70,570,572,574,577,579,582,584,587,589,592,594,597],{"class":72,"line":571},25,[70,573,539],{"class":83},[70,575,576],{"class":181}," status",[70,578,131],{"class":83},[70,580,581],{"class":127}," reactive",[70,583,172],{"class":87},[70,585,586],{"class":127},"Record",[70,588,172],{"class":87},[70,590,591],{"class":181},"string",[70,593,218],{"class":87},[70,595,596],{"class":127},"SaveStatus",[70,598,599],{"class":87},">>({})\n",[70,601,603,606,608,611,614,617,620,622],{"class":72,"line":602},26,[70,604,605],{"class":83},"  for",[70,607,194],{"class":87},[70,609,610],{"class":83},"const",[70,612,613],{"class":181}," path",[70,615,616],{"class":83}," of",[70,618,619],{"class":87}," paths) status[path] ",[70,621,304],{"class":83},[70,623,624],{"class":94}," 'idle'\n",[70,626,628],{"class":72,"line":627},27,[70,629,118],{"emptyLinePlaceholder":117},[70,631,633,636,638,641,643,645,647,649,651,653,655,657,659,661,663,665,667,669],{"class":72,"line":632},28,[70,634,635],{"class":83},"  async",[70,637,349],{"class":83},[70,639,640],{"class":127}," run",[70,642,318],{"class":87},[70,644,417],{"class":200},[70,646,191],{"class":83},[70,648,397],{"class":127},[70,650,172],{"class":87},[70,652,357],{"class":127},[70,654,428],{"class":87},[70,656,431],{"class":200},[70,658,191],{"class":83},[70,660,182],{"class":181},[70,662,218],{"class":87},[70,664,440],{"class":200},[70,666,191],{"class":83},[70,668,445],{"class":127},[70,670,229],{"class":87},[70,672,674,677],{"class":72,"line":673},29,[70,675,676],{"class":83},"    try",[70,678,280],{"class":87},[70,680,682,685,688,690,693,696,699,702,705,708,711,713],{"class":72,"line":681},30,[70,683,684],{"class":83},"      const",[70,686,687],{"class":181}," gate",[70,689,131],{"class":83},[70,691,692],{"class":83}," typeof",[70,694,695],{"class":87}," gateOnValidity ",[70,697,698],{"class":83},"===",[70,700,701],{"class":94}," 'function'",[70,703,704],{"class":83}," ?",[70,706,707],{"class":127}," gateOnValidity",[70,709,710],{"class":87},"(path) ",[70,712,191],{"class":83},[70,714,715],{"class":87}," gateOnValidity\n",[70,717,719,722,725,728,731,733,736,739,742],{"class":72,"line":718},31,[70,720,721],{"class":83},"      if",[70,723,724],{"class":87}," (gate ",[70,726,727],{"class":83},"&&",[70,729,730],{"class":83}," !",[70,732,318],{"class":87},[70,734,735],{"class":83},"await",[70,737,738],{"class":87}," form.",[70,740,741],{"class":127},"validateAsync",[70,743,744],{"class":87},"(path)).success) {\n",[70,746,748,751,753],{"class":72,"line":747},32,[70,749,750],{"class":87},"        status[path] ",[70,752,304],{"class":83},[70,754,624],{"class":94},[70,756,758],{"class":72,"line":757},33,[70,759,760],{"class":83},"        return\n",[70,762,764],{"class":72,"line":763},34,[70,765,766],{"class":87},"      }\n",[70,768,770,772,775],{"class":72,"line":769},35,[70,771,721],{"class":83},[70,773,774],{"class":87}," (signal.aborted) ",[70,776,777],{"class":83},"return\n",[70,779,781,784,786],{"class":72,"line":780},36,[70,782,783],{"class":87},"      status[path] ",[70,785,304],{"class":83},[70,787,788],{"class":94}," 'saving'\n",[70,790,792,795,798],{"class":72,"line":791},37,[70,793,794],{"class":83},"      await",[70,796,797],{"class":127}," save",[70,799,800],{"class":87},"(path, value, signal)\n",[70,802,804,806,808],{"class":72,"line":803},38,[70,805,721],{"class":83},[70,807,774],{"class":87},[70,809,777],{"class":83},[70,811,813,815,817],{"class":72,"line":812},39,[70,814,783],{"class":87},[70,816,304],{"class":83},[70,818,819],{"class":94}," 'saved'\n",[70,821,823,826,829],{"class":72,"line":822},40,[70,824,825],{"class":87},"    } ",[70,827,828],{"class":83},"catch",[70,830,280],{"class":87},[70,832,834,836,838,841,844,846],{"class":72,"line":833},41,[70,835,721],{"class":83},[70,837,194],{"class":87},[70,839,840],{"class":83},"!",[70,842,843],{"class":87},"signal.aborted) status[path] ",[70,845,304],{"class":83},[70,847,155],{"class":94},[70,849,851],{"class":72,"line":850},42,[70,852,853],{"class":87},"    }\n",[70,855,857],{"class":72,"line":856},43,[70,858,329],{"class":87},[70,860,862],{"class":72,"line":861},44,[70,863,118],{"emptyLinePlaceholder":117},[70,865,867,869,871,873,875,877],{"class":72,"line":866},45,[70,868,605],{"class":83},[70,870,194],{"class":87},[70,872,610],{"class":83},[70,874,613],{"class":181},[70,876,616],{"class":83},[70,878,879],{"class":87}," paths) {\n",[70,881,883,886,889,891,893],{"class":72,"line":882},46,[70,884,885],{"class":83},"    const",[70,887,888],{"class":181}," schedule",[70,890,131],{"class":83},[70,892,169],{"class":127},[70,894,895],{"class":87},"(\n",[70,897,899,902,904,906,908,910,912,914,916,918,920,922],{"class":72,"line":898},47,[70,900,901],{"class":87},"      (",[70,903,431],{"class":200},[70,905,191],{"class":83},[70,907,182],{"class":181},[70,909,218],{"class":87},[70,911,440],{"class":200},[70,913,191],{"class":83},[70,915,445],{"class":127},[70,917,209],{"class":87},[70,919,212],{"class":83},[70,921,640],{"class":127},[70,923,924],{"class":87},"(path, value, signal),\n",[70,926,928],{"class":72,"line":927},48,[70,929,930],{"class":87},"      debounceMs\n",[70,932,934],{"class":72,"line":933},49,[70,935,936],{"class":87},"    )\n",[70,938,940,943,946,949,951,953,956,958,960],{"class":72,"line":939},50,[70,941,942],{"class":87},"    form.",[70,944,945],{"class":127},"onChange",[70,947,948],{"class":87},"(path, (",[70,950,431],{"class":200},[70,952,218],{"class":87},[70,954,955],{"class":200},"ctx",[70,957,209],{"class":87},[70,959,212],{"class":83},[70,961,280],{"class":87},[70,963,965,967,969],{"class":72,"line":964},51,[70,966,783],{"class":87},[70,968,304],{"class":83},[70,970,971],{"class":94}," 'pending'\n",[70,973,975,978],{"class":72,"line":974},52,[70,976,977],{"class":127},"      schedule",[70,979,980],{"class":87},"(value, ctx.signal)\n",[70,982,984],{"class":72,"line":983},53,[70,985,986],{"class":87},"    })\n",[70,988,990],{"class":72,"line":989},54,[70,991,329],{"class":87},[70,993,995],{"class":72,"line":994},55,[70,996,118],{"emptyLinePlaceholder":117},[70,998,1000,1002],{"class":72,"line":999},56,[70,1001,263],{"class":83},[70,1003,280],{"class":87},[70,1005,1007],{"class":72,"line":1006},57,[70,1008,1009],{"class":87},"    status,\n",[70,1011,1013,1016,1019,1021,1023,1026,1029,1032,1035,1038,1041,1043,1045,1048,1050,1052],{"class":72,"line":1012},58,[70,1014,1015],{"class":87},"    isSaving: ",[70,1017,1018],{"class":127},"computed",[70,1020,310],{"class":87},[70,1022,212],{"class":83},[70,1024,1025],{"class":87}," Object.",[70,1027,1028],{"class":127},"values",[70,1030,1031],{"class":87},"(status).",[70,1033,1034],{"class":127},"some",[70,1036,1037],{"class":87},"((",[70,1039,1040],{"class":200},"s",[70,1042,209],{"class":87},[70,1044,212],{"class":83},[70,1046,1047],{"class":87}," s ",[70,1049,698],{"class":83},[70,1051,145],{"class":94},[70,1053,1054],{"class":87},")),\n",[70,1056,1058,1061,1063,1065,1067,1070,1073,1075,1077,1079,1081,1084,1086,1089],{"class":72,"line":1057},59,[70,1059,1060],{"class":87},"    failed: ",[70,1062,1018],{"class":127},[70,1064,310],{"class":87},[70,1066,212],{"class":83},[70,1068,1069],{"class":87}," paths.",[70,1071,1072],{"class":127},"filter",[70,1074,1037],{"class":87},[70,1076,417],{"class":200},[70,1078,209],{"class":87},[70,1080,212],{"class":83},[70,1082,1083],{"class":87}," status[path] ",[70,1085,698],{"class":83},[70,1087,1088],{"class":94}," 'error'",[70,1090,1054],{"class":87},[70,1092,1094],{"class":72,"line":1093},60,[70,1095,329],{"class":87},[70,1097,1099],{"class":72,"line":1098},61,[70,1100,335],{"class":87},[17,1102,1103,1104,218,1107,218,1110,1113,1114,1117,1118,1120,1121,1123],{},"The path and form type helpers (",[21,1105,1106],{},"FlatPath",[21,1108,1109],{},"GenericForm",[21,1111,1112],{},"UseFormReturnType",") come from the core ",[21,1115,1116],{},"attaform"," entry. The composable is adapter-agnostic: it works against a Zod v3, Zod v4, or any other adapter's form, since it only reaches for ",[21,1119,945],{}," and ",[21,1122,741],{},".",[44,1125,1127],{"id":1126},"what-it-gives-you-back","What it gives you back",[1129,1130,1131,1147],"table",{},[1132,1133,1134],"thead",{},[1135,1136,1137,1141,1144],"tr",{},[1138,1139,1140],"th",{},"Return",[1138,1142,1143],{},"Type",[1138,1145,1146],{},"Use",[1148,1149,1150,1182,1196],"tbody",{},[1135,1151,1152,1158,1163],{},[1153,1154,1155],"td",{},[21,1156,1157],{},"status",[1153,1159,1160],{},[21,1161,1162],{},"Record\u003Cpath, SaveStatus>",[1153,1164,1165,1166,1169,1170,1169,1173,1169,1176,1169,1179,1123],{},"Per-field badge: ",[21,1167,1168],{},"idle"," \u002F ",[21,1171,1172],{},"pending",[21,1174,1175],{},"saving",[21,1177,1178],{},"saved",[21,1180,1181],{},"error",[1135,1183,1184,1188,1193],{},[1153,1185,1186],{},[21,1187,31],{},[1153,1189,1190],{},[21,1191,1192],{},"ComputedRef\u003Cboolean>",[1153,1194,1195],{},"Aggregate \"a save is in flight\" for a header spinner or banner.",[1135,1197,1198,1203,1208],{},[1153,1199,1200],{},[21,1201,1202],{},"failed",[1153,1204,1205],{},[21,1206,1207],{},"ComputedRef\u003Cpath[]>",[1153,1209,1210],{},"The paths whose last save errored, for a retry affordance.",[17,1212,1213,1214,1216,1217,1219,1220,1169,1223,1226],{},"None of it lives in form state. ",[21,1215,1157],{}," is a plain reactive map the composable owns, exactly the side-channel discipline ",[21,1218,945],{}," is built for: the form's ",[21,1221,1222],{},"dirty",[21,1224,1225],{},"validating"," surface stays about the form, autosave status stays about autosave.",[44,1228,1230],{"id":1229},"wiring-it-up","Wiring it up",[17,1232,1233,1234,1236,1237,1240,1241,1244,1245,1123],{},"Hoist the schema, build the form, then point ",[21,1235,23],{}," at the paths you want saved. The ",[21,1238,1239],{},"save"," callback is yours: it receives the path, its value, and an ",[21,1242,1243],{},"AbortSignal"," to hand straight to ",[21,1246,1247],{},"fetch",[61,1249,1251],{"className":63,"code":1250,"language":65,"meta":66,"style":66},"const schema = z.object({\n  email: z.string().email(),\n  displayName: z.string().min(2),\n  bio: z.string().max(160),\n})\n\nconst form = useForm({ schema })\n\nconst { status, isSaving, failed } = useAutosave(\n  form,\n  ['email', 'displayName', 'bio'],\n  async (path, value, signal) => {\n    await api.patch(`\u002Fprofile\u002F${path}`, { value }, { signal })\n  },\n  { debounceMs: 600 }\n)\n",[21,1252,1253,1271,1287,1307,1326,1331,1335,1350,1354,1378,1383,1404,1426,1450,1455,1466],{"__ignoreMap":66},[70,1254,1255,1257,1260,1262,1265,1268],{"class":72,"line":73},[70,1256,610],{"class":83},[70,1258,1259],{"class":181}," schema",[70,1261,131],{"class":83},[70,1263,1264],{"class":87}," z.",[70,1266,1267],{"class":127},"object",[70,1269,1270],{"class":87},"({\n",[70,1272,1273,1276,1278,1281,1284],{"class":72,"line":80},[70,1274,1275],{"class":87},"  email: z.",[70,1277,591],{"class":127},[70,1279,1280],{"class":87},"().",[70,1282,1283],{"class":127},"email",[70,1285,1286],{"class":87},"(),\n",[70,1288,1289,1292,1294,1296,1299,1301,1304],{"class":72,"line":98},[70,1290,1291],{"class":87},"  displayName: z.",[70,1293,591],{"class":127},[70,1295,1280],{"class":87},[70,1297,1298],{"class":127},"min",[70,1300,318],{"class":87},[70,1302,1303],{"class":181},"2",[70,1305,1306],{"class":87},"),\n",[70,1308,1309,1312,1314,1316,1319,1321,1324],{"class":72,"line":114},[70,1310,1311],{"class":87},"  bio: z.",[70,1313,591],{"class":127},[70,1315,1280],{"class":87},[70,1317,1318],{"class":127},"max",[70,1320,318],{"class":87},[70,1322,1323],{"class":181},"160",[70,1325,1306],{"class":87},[70,1327,1328],{"class":72,"line":121},[70,1329,1330],{"class":87},"})\n",[70,1332,1333],{"class":72,"line":158},[70,1334,118],{"emptyLinePlaceholder":117},[70,1336,1337,1339,1342,1344,1347],{"class":72,"line":163},[70,1338,610],{"class":83},[70,1340,1341],{"class":181}," form",[70,1343,131],{"class":83},[70,1345,1346],{"class":127}," useForm",[70,1348,1349],{"class":87},"({ schema })\n",[70,1351,1352],{"class":72,"line":232},[70,1353,118],{"emptyLinePlaceholder":117},[70,1355,1356,1358,1360,1362,1364,1366,1368,1370,1372,1374,1376],{"class":72,"line":260},[70,1357,610],{"class":83},[70,1359,542],{"class":87},[70,1361,1157],{"class":181},[70,1363,218],{"class":87},[70,1365,31],{"class":181},[70,1367,218],{"class":87},[70,1369,1202],{"class":181},[70,1371,563],{"class":87},[70,1373,304],{"class":83},[70,1375,352],{"class":127},[70,1377,895],{"class":87},[70,1379,1380],{"class":72,"line":283},[70,1381,1382],{"class":87},"  form,\n",[70,1384,1385,1388,1391,1393,1396,1398,1401],{"class":72,"line":298},[70,1386,1387],{"class":87},"  [",[70,1389,1390],{"class":94},"'email'",[70,1392,218],{"class":87},[70,1394,1395],{"class":94},"'displayName'",[70,1397,218],{"class":87},[70,1399,1400],{"class":94},"'bio'",[70,1402,1403],{"class":87},"],\n",[70,1405,1406,1408,1410,1412,1414,1416,1418,1420,1422,1424],{"class":72,"line":326},[70,1407,635],{"class":83},[70,1409,194],{"class":87},[70,1411,417],{"class":200},[70,1413,218],{"class":87},[70,1415,431],{"class":200},[70,1417,218],{"class":87},[70,1419,440],{"class":200},[70,1421,209],{"class":87},[70,1423,212],{"class":83},[70,1425,280],{"class":87},[70,1427,1428,1431,1434,1437,1439,1442,1444,1447],{"class":72,"line":332},[70,1429,1430],{"class":83},"    await",[70,1432,1433],{"class":87}," api.",[70,1435,1436],{"class":127},"patch",[70,1438,318],{"class":87},[70,1440,1441],{"class":94},"`\u002Fprofile\u002F${",[70,1443,417],{"class":87},[70,1445,1446],{"class":94},"}`",[70,1448,1449],{"class":87},", { value }, { signal })\n",[70,1451,1452],{"class":72,"line":338},[70,1453,1454],{"class":87},"  },\n",[70,1456,1457,1460,1463],{"class":72,"line":343},[70,1458,1459],{"class":87},"  { debounceMs: ",[70,1461,1462],{"class":181},"600",[70,1464,1465],{"class":87}," }\n",[70,1467,1468],{"class":72,"line":368},[70,1469,517],{"class":87},[17,1471,1472,1474,1475,1477,1478,1481],{},[21,1473,23],{}," registers its ",[21,1476,945],{}," handlers inside ",[21,1479,1480],{},"setup",", so they stop automatically when the component unmounts. No manual teardown.",[44,1483,1485],{"id":1484},"gating-on-validity","Gating on validity",[17,1487,1488,1489,1491,1492,1495,1496,1500],{},"This is where the two halves meet. ",[21,1490,555],{}," (on by default) runs ",[21,1493,1494],{},"form.validateAsync(path)"," before each save and skips the write when the field is invalid, so a value that fails validation never reaches the server, and the field's ",[54,1497,1499],{"href":1498},"\u002Fdocs\u002Fvalidation\u002Fasync-refinements","async refinements"," run as part of that same check.",[17,1502,1503,1506,1507,1509,1510,1513],{},[21,1504,1505],{},".refine"," decides whether a value is allowed; ",[21,1508,945],{}," decides whether to persist it. Composing them gives you \"save only what passes\" without either concern leaking into the other. Turn the gate off (",[21,1511,1512],{},"{ gateOnValidity: false }",") when you want a true draft autosave that captures even invalid in-progress state.",[17,1515,1516,1517,1520,1521,1523,1524,1527],{},"For per-field control, pass a predicate instead of a boolean: ",[21,1518,1519],{},"gateOnValidity: (path) => path !== 'email'"," keeps ",[21,1522,1283],{}," a draft while gating the rest. The demo above does exactly that, so an invalid email still autosaves while ",[21,1525,1526],{},"fields.email.showErrors"," surfaces the validation message. The write persists the draft, the read flags it, and neither blocks the other.",[44,1529,1531],{"id":1530},"debouncing","Debouncing",[17,1533,1534,1535,1538,1539,1542],{},"A network round-trip per keystroke is wasteful, so each path gets its own debounced scheduler. The recipe inlines a five-line ",[21,1536,1537],{},"debounce"," to stay dependency-free; if your app already uses VueUse, swap in ",[21,1540,1541],{},"useDebounceFn"," with no other change.",[17,1544,1545,1546,1548,1549,1551,1552,1554,1555,1558,1559,1562,1563,1566,1567,1570,1571,1573,1574,1120,1576,1579,1580,1586],{},"Debouncing changes one thing about error handling. Because the scheduler returns immediately, the actual save runs after the ",[21,1547,945],{}," handler has already finished, so a throw inside ",[21,1550,1239],{}," can no longer route to ",[21,1553,945],{},"'s own ",[21,1556,1557],{},"onError",". That is why ",[21,1560,1561],{},"run"," catches its own errors and sets ",[21,1564,1565],{},"status[path] = 'error'",". The ",[21,1568,1569],{},"ctx.signal"," cancellation still works through the debounce: a newer edit aborts the in-flight save's signal regardless of timing, so a stale request to a slow endpoint cancels itself. (For an immediate, undebounced reaction, ",[21,1572,945],{},"'s built-in ",[21,1575,1557],{},[21,1577,1578],{},"retry()"," carry the load instead. See the ",[54,1581,1583,1585],{"href":1582},"\u002Fdocs\u002Fcross-cutting-state\u002Fon-change#errors-never-reach-the-keystroke",[21,1584,945],{}," reference",".)",[44,1588,1590],{"id":1589},"why-a-recipe-not-a-shipped-export","Why a recipe, not a shipped export",[17,1592,1593,1594,1598,1599,1601,1602,1604],{},"Attaform ships ",[54,1595,1597],{"href":1596},"\u002Fdocs\u002Fgetting-started\u002Fwhy-attaform","zero runtime dependencies",", and a genuinely good autosave wants opinions the core shouldn't make for you: the debounce window, retry and backoff policy, whether a save is optimistic or confirmed, and what \"saved\" even means for your backend. Those are application decisions. ",[21,1600,945],{}," is the small, sharp primitive Attaform owns; ",[21,1603,23],{}," is the policy you own, sized and tuned to your app. Copy it, change it, delete the parts you don't need.",[44,1606,1608],{"id":1607},"where-to-next","Where to next",[1610,1611,1612,1620,1626,1640],"ul",{},[1613,1614,1615,1619],"li",{},[54,1616,1617],{"href":56},[21,1618,945],{},": the side-channel primitive this recipe is built on.",[1613,1621,1622,1625],{},[54,1623,1624],{"href":1498},"Async refinements",": the server-side validity checks the gate runs.",[1613,1627,1628,1635,1636,1639],{},[54,1629,1631,1634],{"href":1630},"\u002Fdocs\u002Fwriting-and-mutating\u002Fset-value",[21,1632,1633],{},"setValue"," patterns",": the ",[21,1637,1638],{},"{ silent }"," write for hydrating a form without echoing saves.",[1613,1641,1642,1646],{},[54,1643,1645],{"href":1644},"\u002Fdocs\u002Fcross-cutting-state\u002Fmulti-tab-sync","Multi-tab sync",": keep autosaved forms convergent across a user's tabs.",[1648,1649,1650],"style",{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}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);}",{"title":66,"searchDepth":80,"depth":80,"links":1652},[1653,1654,1655,1656,1657,1658,1659],{"id":46,"depth":80,"text":47},{"id":1126,"depth":80,"text":1127},{"id":1229,"depth":80,"text":1230},{"id":1484,"depth":80,"text":1485},{"id":1530,"depth":80,"text":1531},{"id":1589,"depth":80,"text":1590},{"id":1607,"depth":80,"text":1608},"A copy-paste useAutosave recipe over form.onChange. Per-field status, an aggregate isSaving, a validity gate, and debounced writes, with no new dependency and the save policy in your hands.","md",{},[1664,1667,1670,1673],{"label":1665,"value":1666},"Category","Recipe",{"label":1668,"value":1669,"kind":21},"Built on","form.onChange · validateAsync",{"label":1671,"value":1672,"kind":21},"Returns","status · isSaving · failed",{"label":1674,"value":1675},"Dependency","none (copy-paste, you own it)","\u002Fdocs\u002Fcross-cutting-state\u002Fautosave",{"title":5,"description":1660},null,"docs\u002Fcross-cutting-state\u002Fautosave","vCiJp63HBcNaCgBcaXuEw5mz-Uwv7Kdb36wkTsaJcHA",1781333215225]