[{"data":1,"prerenderedAt":1429},["ShallowReactive",2],{"content-\u002Fdocs\u002Fvalidation\u002Furl-availability-check":3},{"id":4,"title":5,"body":6,"description":1411,"extension":1412,"meta":1413,"metaRows":1414,"navigation":146,"path":1424,"seo":1425,"source":1426,"stem":1427,"__hash__":1428},"docs\u002Fdocs\u002Fvalidation\u002Furl-availability-check.md","URL availability check (preprocess + async refine)",{"type":7,"value":8,"toc":1401},"minimark",[9,13,20,23,28,33,45,64,76,80,99,1049,1053,1056,1076,1086,1103,1107,1206,1209,1213,1221,1243,1247,1250,1297,1308,1312,1367,1371,1397],[10,11,5],"h1",{"id":12},"url-availability-check-preprocess-async-refine",[14,15,16],"blockquote",{},[17,18,19],"p",{},"Two cooperating layers. Preprocess augments the user's input into a full URL. Async refine checks it against a backend, with sentinels for \"empty\" and \"malformed\" so refine can skip the network when there's nothing worth asking. A cache makes repeat checks free.",[21,22],"docs-meta-table",{},[24,25],"docs-demo",{"label":26,"slug":27},"URL Availability Demo","url-availability-check",[29,30,32],"h2",{"id":31},"what-the-form-needs-to-do","What the form needs to do",[17,34,35,36,40,41,44],{},"A signup form takes a site URL. Users type informally (",[37,38,39],"code",{},"example.com",", not ",[37,42,43],{},"https:\u002F\u002Fexample.com","). The form has to:",[46,47,48,52,55,58,61],"ol",{},[49,50,51],"li",{},"Accept whatever the user types verbatim, so the input cursor never jumps under their fingers.",[49,53,54],{},"Augment the value to a full URL when checking it.",[49,56,57],{},"Ask the backend whether the URL is already taken.",[49,59,60],{},"Surface a specific message for each failure mode: empty, malformed, or taken.",[49,62,63],{},"Avoid hammering the backend on every keystroke; reuse previous answers.",[17,65,66,67,71,72,75],{},"Preprocess and async refine map onto that work cleanly. Preprocess ",[68,69,70],"strong",{},"prepares"," the value (trim, add protocol, decide whether it's URL-shaped at all). Refine ",[68,73,74],{},"validates"," it (ask the backend, surface a message). Storage stays as the user's raw text, so re-rendering, persistence, and history all carry the input the user actually typed.",[29,77,79],{"id":78},"the-schema","The schema",[17,81,82,83,86,87,90,91,94,95,98],{},"Hoist the schema next to the form so the type flows through ",[37,84,85],{},"useForm"," cleanly. Two sentinels (",[37,88,89],{},"EMPTY_URL"," and ",[37,92,93],{},"INVALID_URL",") bridge the two layers; both are strings so the inner ",[37,96,97],{},"z.string()"," accepts them, and refine recognizes them by exact equality.",[100,101,106],"pre",{"className":102,"code":103,"language":104,"meta":105,"style":105},"language-ts shiki shiki-themes github-light github-dark","import { useForm } from 'attaform\u002Fzod'\nimport { z } from 'zod'\n\nconst EMPTY_URL = '__atta:empty-url__'\nconst INVALID_URL = '__atta:invalid-url__'\n\nconst TAKEN = new Set(['https:\u002F\u002Fgoogle.com', 'https:\u002F\u002Fapple.com', 'https:\u002F\u002Fgithub.com'])\nconst availabilityCache = new Map\u003Cstring, boolean>()\n\nasync function checkAvailability(url: string): Promise\u003Cboolean> {\n  const cached = availabilityCache.get(url)\n  if (cached !== undefined) return cached\n  await new Promise((r) => setTimeout(r, 350))\n  const available = !TAKEN.has(url)\n  availabilityCache.set(url, available)\n  return available\n}\n\nfunction formatUrl(v: unknown): string {\n  if (typeof v !== 'string') return INVALID_URL\n  const trimmed = v.trim()\n  if (trimmed.length === 0) return EMPTY_URL\n  const withProtocol = \u002F^https?:\\\u002F\\\u002F\u002Fi.test(trimmed) ? trimmed : `https:\u002F\u002F${trimmed}`\n  try {\n    const parsed = new URL(withProtocol)\n    \u002F\u002F WHATWG URL accepts `https:\u002F\u002Fersdg` and `https:\u002F\u002Fa.b` as\n    \u002F\u002F structurally valid. Require a TLD of at least two characters so\n    \u002F\u002F the demo rejects domain-less and 1-char-suffix strings the way\n    \u002F\u002F a real signup form would.\n    const dot = parsed.hostname.lastIndexOf('.')\n    if (dot === -1) return INVALID_URL\n    if (parsed.hostname.length - dot - 1 \u003C 2) return INVALID_URL\n    return parsed.href.replace(\u002F\\\u002F$\u002F, '')\n  } catch {\n    return INVALID_URL\n  }\n}\n\nconst schema = z.object({\n  url: z.preprocess(\n    formatUrl,\n    z.string().refine(\n      async (val) => {\n        if (val === EMPTY_URL || val === INVALID_URL) return false\n        return checkAvailability(val)\n      },\n      {\n        error: (issue) => {\n          const val = issue.input as string\n          if (val === EMPTY_URL) return 'Please enter a URL.'\n          if (val === INVALID_URL) return \"That doesn't look like a URL.\"\n          return `${val} is already taken.`\n        },\n      }\n    )\n  ),\n})\n\nconst form = useForm({ schema, key: 'url-check' })\n","ts","",[37,107,108,128,141,148,164,177,182,219,248,253,293,313,337,370,394,406,415,421,426,454,480,499,523,582,590,609,616,622,628,634,658,682,715,746,757,764,770,775,780,799,811,817,833,850,880,891,897,903,921,941,960,978,992,998,1004,1010,1016,1022,1027],{"__ignoreMap":105},[109,110,113,117,121,124],"span",{"class":111,"line":112},"line",1,[109,114,116],{"class":115},"szBVR","import",[109,118,120],{"class":119},"sVt8B"," { useForm } ",[109,122,123],{"class":115},"from",[109,125,127],{"class":126},"sZZnC"," 'attaform\u002Fzod'\n",[109,129,131,133,136,138],{"class":111,"line":130},2,[109,132,116],{"class":115},[109,134,135],{"class":119}," { z } ",[109,137,123],{"class":115},[109,139,140],{"class":126}," 'zod'\n",[109,142,144],{"class":111,"line":143},3,[109,145,147],{"emptyLinePlaceholder":146},true,"\n",[109,149,151,154,158,161],{"class":111,"line":150},4,[109,152,153],{"class":115},"const",[109,155,157],{"class":156},"sj4cs"," EMPTY_URL",[109,159,160],{"class":115}," =",[109,162,163],{"class":126}," '__atta:empty-url__'\n",[109,165,167,169,172,174],{"class":111,"line":166},5,[109,168,153],{"class":115},[109,170,171],{"class":156}," INVALID_URL",[109,173,160],{"class":115},[109,175,176],{"class":126}," '__atta:invalid-url__'\n",[109,178,180],{"class":111,"line":179},6,[109,181,147],{"emptyLinePlaceholder":146},[109,183,185,187,190,192,195,199,202,205,208,211,213,216],{"class":111,"line":184},7,[109,186,153],{"class":115},[109,188,189],{"class":156}," TAKEN",[109,191,160],{"class":115},[109,193,194],{"class":115}," new",[109,196,198],{"class":197},"sScJk"," Set",[109,200,201],{"class":119},"([",[109,203,204],{"class":126},"'https:\u002F\u002Fgoogle.com'",[109,206,207],{"class":119},", ",[109,209,210],{"class":126},"'https:\u002F\u002Fapple.com'",[109,212,207],{"class":119},[109,214,215],{"class":126},"'https:\u002F\u002Fgithub.com'",[109,217,218],{"class":119},"])\n",[109,220,222,224,227,229,231,234,237,240,242,245],{"class":111,"line":221},8,[109,223,153],{"class":115},[109,225,226],{"class":156}," availabilityCache",[109,228,160],{"class":115},[109,230,194],{"class":115},[109,232,233],{"class":197}," Map",[109,235,236],{"class":119},"\u003C",[109,238,239],{"class":156},"string",[109,241,207],{"class":119},[109,243,244],{"class":156},"boolean",[109,246,247],{"class":119},">()\n",[109,249,251],{"class":111,"line":250},9,[109,252,147],{"emptyLinePlaceholder":146},[109,254,256,259,262,265,268,272,275,278,281,283,286,288,290],{"class":111,"line":255},10,[109,257,258],{"class":115},"async",[109,260,261],{"class":115}," function",[109,263,264],{"class":197}," checkAvailability",[109,266,267],{"class":119},"(",[109,269,271],{"class":270},"s4XuR","url",[109,273,274],{"class":115},":",[109,276,277],{"class":156}," string",[109,279,280],{"class":119},")",[109,282,274],{"class":115},[109,284,285],{"class":197}," Promise",[109,287,236],{"class":119},[109,289,244],{"class":156},[109,291,292],{"class":119},"> {\n",[109,294,296,299,302,304,307,310],{"class":111,"line":295},11,[109,297,298],{"class":115},"  const",[109,300,301],{"class":156}," cached",[109,303,160],{"class":115},[109,305,306],{"class":119}," availabilityCache.",[109,308,309],{"class":197},"get",[109,311,312],{"class":119},"(url)\n",[109,314,316,319,322,325,328,331,334],{"class":111,"line":315},12,[109,317,318],{"class":115},"  if",[109,320,321],{"class":119}," (cached ",[109,323,324],{"class":115},"!==",[109,326,327],{"class":156}," undefined",[109,329,330],{"class":119},") ",[109,332,333],{"class":115},"return",[109,335,336],{"class":119}," cached\n",[109,338,340,343,345,347,350,353,355,358,361,364,367],{"class":111,"line":339},13,[109,341,342],{"class":115},"  await",[109,344,194],{"class":115},[109,346,285],{"class":156},[109,348,349],{"class":119},"((",[109,351,352],{"class":270},"r",[109,354,330],{"class":119},[109,356,357],{"class":115},"=>",[109,359,360],{"class":197}," setTimeout",[109,362,363],{"class":119},"(r, ",[109,365,366],{"class":156},"350",[109,368,369],{"class":119},"))\n",[109,371,373,375,378,380,383,386,389,392],{"class":111,"line":372},14,[109,374,298],{"class":115},[109,376,377],{"class":156}," available",[109,379,160],{"class":115},[109,381,382],{"class":115}," !",[109,384,385],{"class":156},"TAKEN",[109,387,388],{"class":119},".",[109,390,391],{"class":197},"has",[109,393,312],{"class":119},[109,395,397,400,403],{"class":111,"line":396},15,[109,398,399],{"class":119},"  availabilityCache.",[109,401,402],{"class":197},"set",[109,404,405],{"class":119},"(url, available)\n",[109,407,409,412],{"class":111,"line":408},16,[109,410,411],{"class":115},"  return",[109,413,414],{"class":119}," available\n",[109,416,418],{"class":111,"line":417},17,[109,419,420],{"class":119},"}\n",[109,422,424],{"class":111,"line":423},18,[109,425,147],{"emptyLinePlaceholder":146},[109,427,429,432,435,437,440,442,445,447,449,451],{"class":111,"line":428},19,[109,430,431],{"class":115},"function",[109,433,434],{"class":197}," formatUrl",[109,436,267],{"class":119},[109,438,439],{"class":270},"v",[109,441,274],{"class":115},[109,443,444],{"class":156}," unknown",[109,446,280],{"class":119},[109,448,274],{"class":115},[109,450,277],{"class":156},[109,452,453],{"class":119}," {\n",[109,455,457,459,462,465,468,470,473,475,477],{"class":111,"line":456},20,[109,458,318],{"class":115},[109,460,461],{"class":119}," (",[109,463,464],{"class":115},"typeof",[109,466,467],{"class":119}," v ",[109,469,324],{"class":115},[109,471,472],{"class":126}," 'string'",[109,474,330],{"class":119},[109,476,333],{"class":115},[109,478,479],{"class":156}," INVALID_URL\n",[109,481,483,485,488,490,493,496],{"class":111,"line":482},21,[109,484,298],{"class":115},[109,486,487],{"class":156}," trimmed",[109,489,160],{"class":115},[109,491,492],{"class":119}," v.",[109,494,495],{"class":197},"trim",[109,497,498],{"class":119},"()\n",[109,500,502,504,507,510,513,516,518,520],{"class":111,"line":501},22,[109,503,318],{"class":115},[109,505,506],{"class":119}," (trimmed.",[109,508,509],{"class":156},"length",[109,511,512],{"class":115}," ===",[109,514,515],{"class":156}," 0",[109,517,330],{"class":119},[109,519,333],{"class":115},[109,521,522],{"class":156}," EMPTY_URL\n",[109,524,526,528,531,533,536,539,543,546,548,552,555,558,560,563,566,568,571,573,576,579],{"class":111,"line":525},23,[109,527,298],{"class":115},[109,529,530],{"class":156}," withProtocol",[109,532,160],{"class":115},[109,534,535],{"class":126}," \u002F",[109,537,538],{"class":115},"^",[109,540,542],{"class":541},"sA_wV","https",[109,544,545],{"class":115},"?",[109,547,274],{"class":541},[109,549,551],{"class":550},"snhLl","\\\u002F\\\u002F",[109,553,554],{"class":126},"\u002F",[109,556,557],{"class":115},"i",[109,559,388],{"class":119},[109,561,562],{"class":197},"test",[109,564,565],{"class":119},"(trimmed) ",[109,567,545],{"class":115},[109,569,570],{"class":119}," trimmed ",[109,572,274],{"class":115},[109,574,575],{"class":126}," `https:\u002F\u002F${",[109,577,578],{"class":119},"trimmed",[109,580,581],{"class":126},"}`\n",[109,583,585,588],{"class":111,"line":584},24,[109,586,587],{"class":115},"  try",[109,589,453],{"class":119},[109,591,593,596,599,601,603,606],{"class":111,"line":592},25,[109,594,595],{"class":115},"    const",[109,597,598],{"class":156}," parsed",[109,600,160],{"class":115},[109,602,194],{"class":115},[109,604,605],{"class":197}," URL",[109,607,608],{"class":119},"(withProtocol)\n",[109,610,612],{"class":111,"line":611},26,[109,613,615],{"class":614},"sJ8bj","    \u002F\u002F WHATWG URL accepts `https:\u002F\u002Fersdg` and `https:\u002F\u002Fa.b` as\n",[109,617,619],{"class":111,"line":618},27,[109,620,621],{"class":614},"    \u002F\u002F structurally valid. Require a TLD of at least two characters so\n",[109,623,625],{"class":111,"line":624},28,[109,626,627],{"class":614},"    \u002F\u002F the demo rejects domain-less and 1-char-suffix strings the way\n",[109,629,631],{"class":111,"line":630},29,[109,632,633],{"class":614},"    \u002F\u002F a real signup form would.\n",[109,635,637,639,642,644,647,650,652,655],{"class":111,"line":636},30,[109,638,595],{"class":115},[109,640,641],{"class":156}," dot",[109,643,160],{"class":115},[109,645,646],{"class":119}," parsed.hostname.",[109,648,649],{"class":197},"lastIndexOf",[109,651,267],{"class":119},[109,653,654],{"class":126},"'.'",[109,656,657],{"class":119},")\n",[109,659,661,664,667,670,673,676,678,680],{"class":111,"line":660},31,[109,662,663],{"class":115},"    if",[109,665,666],{"class":119}," (dot ",[109,668,669],{"class":115},"===",[109,671,672],{"class":115}," -",[109,674,675],{"class":156},"1",[109,677,330],{"class":119},[109,679,333],{"class":115},[109,681,479],{"class":156},[109,683,685,687,690,692,694,697,700,703,706,709,711,713],{"class":111,"line":684},32,[109,686,663],{"class":115},[109,688,689],{"class":119}," (parsed.hostname.",[109,691,509],{"class":156},[109,693,672],{"class":115},[109,695,696],{"class":119}," dot ",[109,698,699],{"class":115},"-",[109,701,702],{"class":156}," 1",[109,704,705],{"class":115}," \u003C",[109,707,708],{"class":156}," 2",[109,710,330],{"class":119},[109,712,333],{"class":115},[109,714,479],{"class":156},[109,716,718,721,724,727,729,731,734,737,739,741,744],{"class":111,"line":717},33,[109,719,720],{"class":115},"    return",[109,722,723],{"class":119}," parsed.href.",[109,725,726],{"class":197},"replace",[109,728,267],{"class":119},[109,730,554],{"class":126},[109,732,733],{"class":550},"\\\u002F",[109,735,736],{"class":115},"$",[109,738,554],{"class":126},[109,740,207],{"class":119},[109,742,743],{"class":126},"''",[109,745,657],{"class":119},[109,747,749,752,755],{"class":111,"line":748},34,[109,750,751],{"class":119},"  } ",[109,753,754],{"class":115},"catch",[109,756,453],{"class":119},[109,758,760,762],{"class":111,"line":759},35,[109,761,720],{"class":115},[109,763,479],{"class":156},[109,765,767],{"class":111,"line":766},36,[109,768,769],{"class":119},"  }\n",[109,771,773],{"class":111,"line":772},37,[109,774,420],{"class":119},[109,776,778],{"class":111,"line":777},38,[109,779,147],{"emptyLinePlaceholder":146},[109,781,783,785,788,790,793,796],{"class":111,"line":782},39,[109,784,153],{"class":115},[109,786,787],{"class":156}," schema",[109,789,160],{"class":115},[109,791,792],{"class":119}," z.",[109,794,795],{"class":197},"object",[109,797,798],{"class":119},"({\n",[109,800,802,805,808],{"class":111,"line":801},40,[109,803,804],{"class":119},"  url: z.",[109,806,807],{"class":197},"preprocess",[109,809,810],{"class":119},"(\n",[109,812,814],{"class":111,"line":813},41,[109,815,816],{"class":119},"    formatUrl,\n",[109,818,820,823,825,828,831],{"class":111,"line":819},42,[109,821,822],{"class":119},"    z.",[109,824,239],{"class":197},[109,826,827],{"class":119},"().",[109,829,830],{"class":197},"refine",[109,832,810],{"class":119},[109,834,836,839,841,844,846,848],{"class":111,"line":835},43,[109,837,838],{"class":115},"      async",[109,840,461],{"class":119},[109,842,843],{"class":270},"val",[109,845,330],{"class":119},[109,847,357],{"class":115},[109,849,453],{"class":119},[109,851,853,856,859,861,863,866,869,871,873,875,877],{"class":111,"line":852},44,[109,854,855],{"class":115},"        if",[109,857,858],{"class":119}," (val ",[109,860,669],{"class":115},[109,862,157],{"class":156},[109,864,865],{"class":115}," ||",[109,867,868],{"class":119}," val ",[109,870,669],{"class":115},[109,872,171],{"class":156},[109,874,330],{"class":119},[109,876,333],{"class":115},[109,878,879],{"class":156}," false\n",[109,881,883,886,888],{"class":111,"line":882},45,[109,884,885],{"class":115},"        return",[109,887,264],{"class":197},[109,889,890],{"class":119},"(val)\n",[109,892,894],{"class":111,"line":893},46,[109,895,896],{"class":119},"      },\n",[109,898,900],{"class":111,"line":899},47,[109,901,902],{"class":119},"      {\n",[109,904,906,909,912,915,917,919],{"class":111,"line":905},48,[109,907,908],{"class":197},"        error",[109,910,911],{"class":119},": (",[109,913,914],{"class":270},"issue",[109,916,330],{"class":119},[109,918,357],{"class":115},[109,920,453],{"class":119},[109,922,924,927,930,932,935,938],{"class":111,"line":923},49,[109,925,926],{"class":115},"          const",[109,928,929],{"class":156}," val",[109,931,160],{"class":115},[109,933,934],{"class":119}," issue.input ",[109,936,937],{"class":115},"as",[109,939,940],{"class":156}," string\n",[109,942,944,947,949,951,953,955,957],{"class":111,"line":943},50,[109,945,946],{"class":115},"          if",[109,948,858],{"class":119},[109,950,669],{"class":115},[109,952,157],{"class":156},[109,954,330],{"class":119},[109,956,333],{"class":115},[109,958,959],{"class":126}," 'Please enter a URL.'\n",[109,961,963,965,967,969,971,973,975],{"class":111,"line":962},51,[109,964,946],{"class":115},[109,966,858],{"class":119},[109,968,669],{"class":115},[109,970,171],{"class":156},[109,972,330],{"class":119},[109,974,333],{"class":115},[109,976,977],{"class":126}," \"That doesn't look like a URL.\"\n",[109,979,981,984,987,989],{"class":111,"line":980},52,[109,982,983],{"class":115},"          return",[109,985,986],{"class":126}," `${",[109,988,843],{"class":119},[109,990,991],{"class":126},"} is already taken.`\n",[109,993,995],{"class":111,"line":994},53,[109,996,997],{"class":119},"        },\n",[109,999,1001],{"class":111,"line":1000},54,[109,1002,1003],{"class":119},"      }\n",[109,1005,1007],{"class":111,"line":1006},55,[109,1008,1009],{"class":119},"    )\n",[109,1011,1013],{"class":111,"line":1012},56,[109,1014,1015],{"class":119},"  ),\n",[109,1017,1019],{"class":111,"line":1018},57,[109,1020,1021],{"class":119},"})\n",[109,1023,1025],{"class":111,"line":1024},58,[109,1026,147],{"emptyLinePlaceholder":146},[109,1028,1030,1032,1035,1037,1040,1043,1046],{"class":111,"line":1029},59,[109,1031,153],{"class":115},[109,1033,1034],{"class":156}," form",[109,1036,160],{"class":115},[109,1038,1039],{"class":197}," useForm",[109,1041,1042],{"class":119},"({ schema, key: ",[109,1044,1045],{"class":126},"'url-check'",[109,1047,1048],{"class":119}," })\n",[29,1050,1052],{"id":1051},"how-the-layers-split-the-work","How the layers split the work",[17,1054,1055],{},"Preprocess and refine each own a single decision.",[17,1057,1058,1061,1062,1065,1066,1071,1072,1075],{},[68,1059,1060],{},"Preprocess decides what shape the value is in."," It receives whatever the consumer wrote (",[37,1063,1064],{},"unknown"," at the type level under the ",[1067,1068,1070],"a",{"href":1069},"\u002Fdocs\u002Fschemas\u002Fstorage-shape","storage contract",") and produces one of three string outputs: a fully-qualified URL, the empty sentinel, or the invalid sentinel. The function is synchronous, so it never holds up the write boundary; storage at ",[37,1073,1074],{},"form.values.url"," keeps the raw text the user typed.",[17,1077,1078,1081,1082,1085],{},[68,1079,1080],{},"Refine validates the prepared value."," It sees the post-preprocess string. Sentinels short-circuit straight to failure; real URLs go through ",[37,1083,1084],{},"checkAvailability",". Refine also owns the error messages: it has all the context (the value, the sentinel) to pick the right copy without preprocess having to thread it through.",[17,1087,1088,1091,1092,1095,1096,1099,1100,1102],{},[68,1089,1090],{},"The cache is a Map keyed on the post-preprocess URL."," Repeat checks against the same URL hit the cache instead of the simulated network, so a user who types ",[37,1093,1094],{},"google.com",", edits it to ",[37,1097,1098],{},"apple.com",", then back to ",[37,1101,1094],{}," only pays the API cost twice. In production, you'd populate the cache from a SWR \u002F TanStack Query layer rather than rolling your own.",[29,1104,1106],{"id":1105},"three-messages-one-validator","Three messages, one validator",[1108,1109,1110,1129],"table",{},[1111,1112,1113],"thead",{},[1114,1115,1116,1120,1123,1126],"tr",{},[1117,1118,1119],"th",{},"User input",[1117,1121,1122],{},"Preprocess returns",[1117,1124,1125],{},"Refine outcome",[1117,1127,1128],{},"Error message",[1130,1131,1132,1150,1166,1188],"tbody",{},[1114,1133,1134,1140,1144,1147],{},[1135,1136,1137,1139],"td",{},[37,1138,743],{}," (empty input)",[1135,1141,1142],{},[37,1143,89],{},[1135,1145,1146],{},"invalid",[1135,1148,1149],{},"\"Please enter a URL.\"",[1114,1151,1152,1157,1161,1163],{},[1135,1153,1154],{},[37,1155,1156],{},"'###'",[1135,1158,1159],{},[37,1160,93],{},[1135,1162,1146],{},[1135,1164,1165],{},"\"That doesn't look like a URL.\"",[1114,1167,1168,1173,1177,1179],{},[1135,1169,1170],{},[37,1171,1172],{},"'google.com'",[1135,1174,1175],{},[37,1176,204],{},[1135,1178,1146],{},[1135,1180,1181,1182,1187],{},"\"",[1067,1183,1184],{"href":1184,"rel":1185},"https:\u002F\u002Fgoogle.com",[1186],"nofollow"," is already taken.\"",[1114,1189,1190,1195,1200,1203],{},[1135,1191,1192],{},[37,1193,1194],{},"'attaform.dev'",[1135,1196,1197],{},[37,1198,1199],{},"'https:\u002F\u002Fattaform.dev'",[1135,1201,1202],{},"valid",[1135,1204,1205],{},"(none, form submits)",[17,1207,1208],{},"Every message comes out of the refine layer. Preprocess never raises an error itself; it just hands a value over for refine to grade. This is the cleanest division: validation messages all live in one place.",[29,1210,1212],{"id":1211},"storage-stays-raw","Storage stays raw",[17,1214,1215,1216,207,1218,1220],{},"Under the ",[1067,1217,1070],{"href":1069},[37,1219,1074],{}," holds whatever the user typed, never the post-preprocess value and never the sentinel. The directive re-renders the input from storage, so the user always sees their own text. The sentinels live only inside the parse pipeline (preprocess output → refine input); they never leak to the surface.",[17,1222,1223,1224,1226,1227,1229,1230,1232,1233,207,1236,1239,1240,388],{},"This split is what makes the recipe work. Under a \"preprocess mutates storage at write\" model, typing ",[37,1225,1094],{}," would jump to ",[37,1228,1184],{}," mid-edit, the cursor would land in the middle of \"google\", and the sentinel pattern would surface in ",[37,1231,1074],{}," as a magic string. Under the no-write-mutation contract, the typed value is the displayed value is the stored value, and the parsed view is reached through ",[37,1234,1235],{},"handleSubmit",[37,1237,1238],{},"validate",", or ",[37,1241,1242],{},"validateAsync",[29,1244,1246],{"id":1245},"reaching-the-typed-result","Reaching the typed result",[17,1248,1249],{},"Submit hands the success callback the post-parse output: preprocess has augmented the URL, refine has confirmed availability, and the cache holds the answer for the next pass.",[100,1251,1253],{"className":102,"code":1252,"language":104,"meta":105,"style":105},"const onSubmit = form.handleSubmit((data) => {\n  data.url \u002F\u002F 'https:\u002F\u002Fattaform.dev', full URL, augmented + checked\n  \u002F\u002F POST to the signup endpoint…\n})\n",[37,1254,1255,1280,1288,1293],{"__ignoreMap":105},[109,1256,1257,1259,1262,1264,1267,1269,1271,1274,1276,1278],{"class":111,"line":112},[109,1258,153],{"class":115},[109,1260,1261],{"class":156}," onSubmit",[109,1263,160],{"class":115},[109,1265,1266],{"class":119}," form.",[109,1268,1235],{"class":197},[109,1270,349],{"class":119},[109,1272,1273],{"class":270},"data",[109,1275,330],{"class":119},[109,1277,357],{"class":115},[109,1279,453],{"class":119},[109,1281,1282,1285],{"class":111,"line":130},[109,1283,1284],{"class":119},"  data.url ",[109,1286,1287],{"class":614},"\u002F\u002F 'https:\u002F\u002Fattaform.dev', full URL, augmented + checked\n",[109,1289,1290],{"class":111,"line":143},[109,1291,1292],{"class":614},"  \u002F\u002F POST to the signup endpoint…\n",[109,1294,1295],{"class":111,"line":150},[109,1296,1021],{"class":119},[17,1298,1299,1300,1303,1304,1307],{},"If you need the typed shape outside submit, call ",[37,1301,1302],{},"form.validateAsync()"," or ",[37,1305,1306],{},"form.parse()",". Both run the same pipeline against current storage and return the post-parse output.",[29,1309,1311],{"id":1310},"tweaks","Tweaks",[1313,1314,1315,1326,1344,1354],"ul",{},[49,1316,1317,1320,1321,90,1323,1325],{},[68,1318,1319],{},"One sentinel instead of two."," If you only need a single \"rejected\" message (\"That URL won't work\"), collapse ",[37,1322,89],{},[37,1324,93],{}," into one sentinel and short-circuit refine on that single check. You lose the empty-vs-malformed distinction but the schema gets a few lines shorter.",[49,1327,1328,1331,1332,1335,1336,1339,1340,1343],{},[68,1329,1330],{},"Throttle the network round-trip."," The form checks on every change by default. For a network-backed check you'll usually want ",[37,1333,1334],{},"validateOn: 'blur'"," (hit the endpoint when the field loses focus) or ",[37,1337,1338],{},"validateOn: 'submit'"," (defer entirely), optionally paired with ",[37,1341,1342],{},"debounceMs"," to coalesce bursts. The cache absorbs repeat checks regardless.",[49,1345,1346,1349,1350,1353],{},[68,1347,1348],{},"Invalidate on success."," After a successful signup, call ",[37,1351,1352],{},"availabilityCache.delete(submittedUrl)"," so a subsequent re-check picks up the new \"taken\" state on the server.",[49,1355,1356,1359,1360,1363,1364,1366],{},[68,1357,1358],{},"Real backend."," Swap the ",[37,1361,1362],{},"setTimeout","-based ",[37,1365,1084],{}," for your API call. A library like TanStack Query gives you the cache + de-duplication for free; the schema stays exactly as written.",[29,1368,1370],{"id":1369},"where-to-next","Where to next",[1313,1372,1373,1379,1386],{},[49,1374,1375,1378],{},[1067,1376,1377],{"href":1069},"The storage contract",": why preprocess and coerce keep storage raw, and how the two-layer model splits responsibilities.",[49,1380,1381,1385],{},[1067,1382,1384],{"href":1383},"\u002Fdocs\u002Fvalidation\u002Fasync-refinements","Async refinements",": the broader lifecycle around async refine, debouncing, and the validating state.",[49,1387,1388,1392,1393,1396],{},[1067,1389,1391],{"href":1390},"\u002Fdocs\u002Fvalidation\u002Fshowing-errors","Display state and showing errors",": when to render ",[37,1394,1395],{},"form.errors.url"," so the user sees the message at the right moment.",[1398,1399,1400],"style",{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sA_wV, html code.shiki .sA_wV{--shiki-default:#032F62;--shiki-dark:#DBEDFF}html pre.shiki code .snhLl, html code.shiki .snhLl{--shiki-default:#22863A;--shiki-default-font-weight:bold;--shiki-dark:#85E89D;--shiki-dark-font-weight:bold}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}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":105,"searchDepth":130,"depth":130,"links":1402},[1403,1404,1405,1406,1407,1408,1409,1410],{"id":31,"depth":130,"text":32},{"id":78,"depth":130,"text":79},{"id":1051,"depth":130,"text":1052},{"id":1105,"depth":130,"text":1106},{"id":1211,"depth":130,"text":1212},{"id":1245,"depth":130,"text":1246},{"id":1310,"depth":130,"text":1311},{"id":1369,"depth":130,"text":1370},"Preprocess prepares the value, async refine validates it. A small sentinel handoff lets refine skip the API call when preprocess already knows the input is empty or malformed, and a cache keeps repeat checks free.","md",{},[1415,1418,1421],{"label":1416,"value":1417},"Category","Recipe",{"label":1419,"value":1420,"kind":37},"Builds on","z.preprocess, .refine, useForm",{"label":1422,"value":1423,"kind":37},"Surfaces","form.errors, form.meta.validating","\u002Fdocs\u002Fvalidation\u002Furl-availability-check",{"title":5,"description":1411},null,"docs\u002Fvalidation\u002Furl-availability-check","McpwRkPM5HkVaQeovNbnSTnY7BZD_o-sUOVbY5I8bNc",1780949760317]