[{"data":1,"prerenderedAt":497},["ShallowReactive",2],{"content-\u002Fdocs\u002Fvalidation\u002Fasync-refinements":3},{"id":4,"title":5,"body":6,"description":477,"extension":478,"meta":479,"metaRows":480,"navigation":491,"path":492,"seo":493,"source":494,"stem":495,"__hash__":496},"docs\u002Fdocs\u002Fvalidation\u002Fasync-refinements.md","Async refinements",{"type":7,"value":8,"toc":467},"minimark",[9,13,25,28,47,51,56,66,138,149,153,156,177,180,218,224,231,234,257,268,272,288,325,328,335,378,387,391,402,406,413,423,427,463],[10,11,5],"h1",{"id":12},"async-refinements",[14,15,16],"blockquote",{},[17,18,19,20,24],"p",{},"Predicates that await a server round-trip: uniqueness probes, slug availability, password-breach lookups. Same surface as sync refinements, just with ",[21,22,23],"code",{},"async",".",[26,27],"docs-meta-table",{},[17,29,30,31,34,35,38,39,42,43,46],{},"Type a username and blur the field to watch ",[21,32,33],{},"validating"," flip true for ~700ms while the simulated check runs. Try ",[21,36,37],{},"ada",", ",[21,40,41],{},"champ",", or ",[21,44,45],{},"athlete"," to see the \"taken\" error land. Try any unused name to see it accept. The submit handler awaits every in-flight refinement before dispatching; submitting mid-check holds until the check resolves.",[48,49],"docs-demo",{"label":50,"slug":12},"Async Refinements Demo",[52,53,55],"h2",{"id":54},"declare-an-async-predicate","Declare an async predicate",[17,57,58,59,62,63,65],{},"Zod's ",[21,60,61],{},".refine"," accepts an ",[21,64,23],{}," function:",[67,68,73],"pre",{"className":69,"code":70,"language":71,"meta":72,"style":72},"language-ts shiki shiki-themes github-light github-dark","z.string().refine(async (v) => isAvailable(v), {\n  message: 'That username is taken',\n})\n","ts","",[21,74,75,119,132],{"__ignoreMap":72},[76,77,80,84,88,91,94,97,100,103,107,110,113,116],"span",{"class":78,"line":79},"line",1,[76,81,83],{"class":82},"sVt8B","z.",[76,85,87],{"class":86},"sScJk","string",[76,89,90],{"class":82},"().",[76,92,93],{"class":86},"refine",[76,95,96],{"class":82},"(",[76,98,23],{"class":99},"szBVR",[76,101,102],{"class":82}," (",[76,104,106],{"class":105},"s4XuR","v",[76,108,109],{"class":82},") ",[76,111,112],{"class":99},"=>",[76,114,115],{"class":86}," isAvailable",[76,117,118],{"class":82},"(v), {\n",[76,120,122,125,129],{"class":78,"line":121},2,[76,123,124],{"class":82},"  message: ",[76,126,128],{"class":127},"sZZnC","'That username is taken'",[76,130,131],{"class":82},",\n",[76,133,135],{"class":78,"line":134},3,[76,136,137],{"class":82},"})\n",[17,139,140,141,144,145,148],{},"The predicate runs alongside sync refinements; the chain awaits its resolution before deciding pass \u002F fail. Attaform forwards the awaited result to ",[21,142,143],{},"form.errors.\u003Cpath>"," and ",[21,146,147],{},"fields.\u003Cpath>"," like any other refinement.",[52,150,152],{"id":151},"in-flight-signal","In-flight signal",[17,154,155],{},"While an async refinement is pending at a path:",[157,158,159,169],"ul",{},[160,161,162,165,166,24],"li",{},[21,163,164],{},"fields.\u003Cpath>.validating"," is ",[21,167,168],{},"true",[160,170,171,165,174,176],{},[21,172,173],{},"meta.validating",[21,175,168],{}," when ANY field has a pending async refinement.",[17,178,179],{},"Render a \"Checking…\" indicator next to the field:",[67,181,185],{"className":182,"code":183,"language":184,"meta":72,"style":72},"language-vue shiki shiki-themes github-light github-dark","\u003Csmall v-if=\"fields.username.validating\">Checking availability…\u003C\u002Fsmall>\n","vue",[21,186,187],{"__ignoreMap":72},[76,188,189,192,196,199,202,205,208,210,213,215],{"class":78,"line":79},[76,190,191],{"class":82},"\u003C",[76,193,195],{"class":194},"s9eBZ","small",[76,197,198],{"class":99}," v-if",[76,200,201],{"class":82},"=",[76,203,204],{"class":127},"\"",[76,206,207],{"class":82},"fields.username.validating",[76,209,204],{"class":127},[76,211,212],{"class":82},">Checking availability…\u003C\u002F",[76,214,195],{"class":194},[76,216,217],{"class":82},">\n",[17,219,220,221,223],{},"This is per-field UX; for a form-level spinner reach for ",[21,222,173],{}," instead.",[52,225,227,230],{"id":226},"handlesubmit-awaits",[21,228,229],{},"handleSubmit"," awaits",[17,232,233],{},"The submit handler waits for every pending async refinement before deciding pass \u002F fail:",[235,236,237,240,243,250],"ol",{},[160,238,239],{},"Sync validation runs across every active path.",[160,241,242],{},"Async refinements await.",[160,244,245,246,249],{},"If every refinement passes, ",[21,247,248],{},"onSubmit(values)"," fires with the parsed Zod output.",[160,251,252,253,256],{},"If anything fails, focus pulls to the first invalid field and ",[21,254,255],{},"onError(errors)"," fires.",[17,258,259,260,263,264,267],{},"Submitting mid-check is safe: the handler holds until the check resolves, then routes through ",[21,261,262],{},"onSubmit"," or ",[21,265,266],{},"onError",". No flash-of-valid window where the user hits submit while a slow uniqueness probe hasn't finished.",[52,269,271],{"id":270},"debouncing-keystroke-triggers","Debouncing keystroke triggers",[17,273,274,275,278,279,283,284,287],{},"By default, sync refinements run on every committed write (with ",[21,276,277],{},"validateOn: 'change'",", which pairs with the directive's per-keystroke commit). For async refinements, you usually want ",[280,281,282],"strong",{},"blur",": server probes shouldn't fire on every keystroke. Set ",[21,285,286],{},"validateOn: 'blur'"," per form:",[67,289,291],{"className":69,"code":290,"language":71,"meta":72,"style":72},"useForm({\n  schema,\n  validateOn: 'blur', \u002F\u002F async probes fire on blur, not keystroke\n})\n",[21,292,293,301,306,320],{"__ignoreMap":72},[76,294,295,298],{"class":78,"line":79},[76,296,297],{"class":86},"useForm",[76,299,300],{"class":82},"({\n",[76,302,303],{"class":78,"line":121},[76,304,305],{"class":82},"  schema,\n",[76,307,308,311,314,316],{"class":78,"line":134},[76,309,310],{"class":82},"  validateOn: ",[76,312,313],{"class":127},"'blur'",[76,315,38],{"class":82},[76,317,319],{"class":318},"sJ8bj","\u002F\u002F async probes fire on blur, not keystroke\n",[76,321,323],{"class":78,"line":322},4,[76,324,137],{"class":82},[17,326,327],{},"Blur fires the probe only when the value actually changed since the last pass, so refocusing a field and tabbing away without editing it won't re-hit the server.",[17,329,330,331,334],{},"Or stay on the per-keystroke trigger and coalesce bursts with ",[21,332,333],{},"debounceMs",":",[67,336,338],{"className":69,"code":337,"language":71,"meta":72,"style":72},"useForm({\n  schema,\n  validateOn: 'change',\n  debounceMs: 400, \u002F\u002F wait 400ms of quiet before validating\n})\n",[21,339,340,346,350,359,373],{"__ignoreMap":72},[76,341,342,344],{"class":78,"line":79},[76,343,297],{"class":86},[76,345,300],{"class":82},[76,347,348],{"class":78,"line":121},[76,349,305],{"class":82},[76,351,352,354,357],{"class":78,"line":134},[76,353,310],{"class":82},[76,355,356],{"class":127},"'change'",[76,358,131],{"class":82},[76,360,361,364,368,370],{"class":78,"line":322},[76,362,363],{"class":82},"  debounceMs: ",[76,365,367],{"class":366},"sj4cs","400",[76,369,38],{"class":82},[76,371,372],{"class":318},"\u002F\u002F wait 400ms of quiet before validating\n",[76,374,376],{"class":78,"line":375},5,[76,377,137],{"class":82},[17,379,380,381,386],{},"See ",[382,383,385],"a",{"href":384},"\u002Fdocs\u002Fvalidation\u002Fwhen-validation-runs","When validation runs"," for the full timing API.",[52,388,390],{"id":389},"race-safety","Race-safety",[17,392,393,394,397,398,401],{},"Two rapid edits before the first probe returns: the directive cancels the stale in-flight request (where the underlying client supports ",[21,395,396],{},"AbortSignal",") or ignores its result (where it doesn't). The newest probe's result is the one that lands in ",[21,399,400],{},"errors.\u003Cpath>",". No \"earlier request resolves last, overwrites the correct error\" race.",[52,403,405],{"id":404},"validation-not-persistence","Validation, not persistence",[17,407,408,409,223],{},"An async refinement's job is to return a verdict: it reads a value and answers \"is this allowed?\". Writing to your server inside that same loop (saving the value while you check it) is tempting, but it tangles two concerns, validity and persistence, into one predicate. Keep the refinement a pure read, and persist on change through an ",[382,410,412],{"href":411},"\u002Fdocs\u002Fcross-cutting-state\u002Fautosave","autosave",[17,414,415,416,418,419,422],{},"The two compose cleanly. An ",[382,417,412],{"href":411}," handler can gate its write on ",[21,420,421],{},"await form.validateAsync(path)",", so the refinement you wrote here decides whether the save fires. One value check, reused for both the error message and the save gate.",[52,424,426],{"id":425},"where-to-next","Where to next",[157,428,429,435,446,455],{},[160,430,431,434],{},[382,432,433],{"href":411},"Autosave",": the watch-based recipe for persisting values as they change.",[160,436,437,441,442,445],{},[382,438,440],{"href":439},"\u002Fdocs\u002Fvalidation\u002Flifecycle","The validation lifecycle",": the imperative ",[21,443,444],{},"validateAsync()"," for non-submit code paths.",[160,447,448,450,451,454],{},[382,449,385],{"href":384},": the ",[21,452,453],{},"validateOn"," cadence knob.",[160,456,457,462],{},[382,458,460],{"href":459},"\u002Fdocs\u002Fsubmitting\u002Fhandle-submit",[21,461,229],{},": the dispatch surface that awaits async refinements.",[464,465,466],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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);}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":72,"searchDepth":121,"depth":121,"links":468},[469,470,471,473,474,475,476],{"id":54,"depth":121,"text":55},{"id":151,"depth":121,"text":152},{"id":226,"depth":121,"text":472},"handleSubmit awaits",{"id":270,"depth":121,"text":271},{"id":389,"depth":121,"text":390},{"id":404,"depth":121,"text":405},{"id":425,"depth":121,"text":426},"Zod's async .refine predicates run alongside sync ones. The directive surfaces fields.\u003Cpath>.validating while they're in flight, and handleSubmit awaits every pending refinement before dispatch.","md",{},[481,484,487,488],{"label":482,"value":483},"Category","Schema pattern",{"label":485,"value":486},"Triggers","validateOn cadence + handleSubmit gate",{"label":152,"value":164,"kind":21},{"label":489,"value":490},"Submit awaits","Yes, every pending refinement before onSubmit",true,"\u002Fdocs\u002Fvalidation\u002Fasync-refinements",{"title":5,"description":477},null,"docs\u002Fvalidation\u002Fasync-refinements","dg6Q8j47jcCO48j5Ua4KkUvEcFNHqoGV6FHYYjVt1mo",1781745873318]