[{"data":1,"prerenderedAt":469},["ShallowReactive",2],{"content-\u002Fdocs\u002Fvalidation\u002Fasync-refinements":3},{"id":4,"title":5,"body":6,"description":449,"extension":450,"meta":451,"metaRows":452,"navigation":463,"path":464,"seo":465,"source":466,"stem":467,"__hash__":468},"docs\u002Fdocs\u002Fvalidation\u002Fasync-refinements.md","Async refinements",{"type":7,"value":8,"toc":440},"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,436],[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},"where-to-next","Where to next",[157,407,408,419,428],{},[160,409,410,414,415,418],{},[382,411,413],{"href":412},"\u002Fdocs\u002Fvalidation\u002Flifecycle","The validation lifecycle",": the imperative ",[21,416,417],{},"validateAsync()"," for non-submit code paths.",[160,420,421,423,424,427],{},[382,422,385],{"href":384},": the ",[21,425,426],{},"validateOn"," cadence knob.",[160,429,430,435],{},[382,431,433],{"href":432},"\u002Fdocs\u002Fsubmitting\u002Fhandle-submit",[21,434,229],{},": the dispatch surface that awaits async refinements.",[437,438,439],"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":441},[442,443,444,446,447,448],{"id":54,"depth":121,"text":55},{"id":151,"depth":121,"text":152},{"id":226,"depth":121,"text":445},"handleSubmit awaits",{"id":270,"depth":121,"text":271},{"id":389,"depth":121,"text":390},{"id":404,"depth":121,"text":405},"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",{},[453,456,459,460],{"label":454,"value":455},"Category","Schema pattern",{"label":457,"value":458},"Triggers","validateOn cadence + handleSubmit gate",{"label":152,"value":164,"kind":21},{"label":461,"value":462},"Submit awaits","Yes, every pending refinement before onSubmit",true,"\u002Fdocs\u002Fvalidation\u002Fasync-refinements",{"title":5,"description":449},null,"docs\u002Fvalidation\u002Fasync-refinements","ingQ5s8pO01mPzZEQZccNqRw3jGmv4WuAHSLshHyPB4",1780949760243]