[{"data":1,"prerenderedAt":835},["ShallowReactive",2],{"content-\u002Fdocs\u002Frecipes\u002Fssr-hydration":3},{"id":4,"title":5,"body":6,"description":16,"extension":829,"meta":830,"navigation":260,"path":831,"seo":832,"stem":833,"__hash__":834},"docs\u002Fdocs\u002Frecipes\u002Fssr-hydration.md","SSR hydration (Nuxt + bare Vue)",{"type":7,"value":8,"toc":816},"minimark",[9,13,17,20,41,46,53,108,172,187,191,200,434,438,546,553,696,702,706,726,730,735,767,772,782,787,797,801,812],[10,11,5],"h1",{"id":12},"ssr-hydration-nuxt-bare-vue",[14,15,16],"p",{},"Server-rendered form values, errors, and field flags round-trip to\nthe client automatically — no \"loading → hydrated\" flicker.",[14,18,19],{},"Two setups covered:",[21,22,23,31],"ul",{},[24,25,26,30],"li",{},[27,28,29],"strong",{},"Nuxt 3 \u002F 4"," — you don't do anything. The module handles it.",[24,32,33,40],{},[27,34,35,36],{},"Bare Vue 3 + ",[37,38,39],"code",{},"@vue\u002Fserver-renderer"," — two one-liners bridge\nthe server → client boundary.",[42,43,45],"h2",{"id":44},"nuxt-nothing-to-wire","Nuxt — nothing to wire",[14,47,48,49,52],{},"Install the module and call ",[37,50,51],{},"useForm"," normally:",[54,55,60],"pre",{"className":56,"code":57,"language":58,"meta":59,"style":59},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: ['attaform\u002Fnuxt'],\n})\n","ts","",[37,61,62,71,89,102],{"__ignoreMap":59},[63,64,67],"span",{"class":65,"line":66},"line",1,[63,68,70],{"class":69},"sJ8bj","\u002F\u002F nuxt.config.ts\n",[63,72,74,78,81,85],{"class":65,"line":73},2,[63,75,77],{"class":76},"szBVR","export",[63,79,80],{"class":76}," default",[63,82,84],{"class":83},"sScJk"," defineNuxtConfig",[63,86,88],{"class":87},"sVt8B","({\n",[63,90,92,95,99],{"class":65,"line":91},3,[63,93,94],{"class":87},"  modules: [",[63,96,98],{"class":97},"sZZnC","'attaform\u002Fnuxt'",[63,100,101],{"class":87},"],\n",[63,103,105],{"class":65,"line":104},4,[63,106,107],{"class":87},"})\n",[54,109,113],{"className":110,"code":111,"language":112,"meta":59,"style":59},"language-vue shiki shiki-themes github-light github-dark","\u003Cscript setup lang=\"ts\">\n  const form = useForm({ schema, key: 'signup' })\n\u003C\u002Fscript>\n","vue",[37,114,115,139,163],{"__ignoreMap":59},[63,116,117,120,124,127,130,133,136],{"class":65,"line":66},[63,118,119],{"class":87},"\u003C",[63,121,123],{"class":122},"s9eBZ","script",[63,125,126],{"class":83}," setup",[63,128,129],{"class":83}," lang",[63,131,132],{"class":87},"=",[63,134,135],{"class":97},"\"ts\"",[63,137,138],{"class":87},">\n",[63,140,141,144,148,151,154,157,160],{"class":65,"line":73},[63,142,143],{"class":76},"  const",[63,145,147],{"class":146},"sj4cs"," form",[63,149,150],{"class":76}," =",[63,152,153],{"class":83}," useForm",[63,155,156],{"class":87},"({ schema, key: ",[63,158,159],{"class":97},"'signup'",[63,161,162],{"class":87}," })\n",[63,164,165,168,170],{"class":65,"line":91},[63,166,167],{"class":87},"\u003C\u002F",[63,169,123],{"class":122},[63,171,138],{"class":87},[14,173,174,175,178,179,182,183,186],{},"Values, errors, and touched \u002F focused \u002F blurred flags survive the\nserver → client round-trip through ",[37,176,177],{},"nuxtApp.payload",". Need to peek?\nOpen the rendered HTML and look for your Nuxt payload ",[37,180,181],{},"\u003Cscript>",";\n",[37,184,185],{},"attaform"," is a top-level key.",[42,188,190],{"id":189},"bare-vue-two-functions","Bare Vue — two functions",[192,193,195,196,199],"h3",{"id":194},"server-entry-serverts","Server (",[37,197,198],{},"entry-server.ts",")",[54,201,203],{"className":56,"code":202,"language":58,"meta":59,"style":59},"import { createSSRApp } from 'vue'\nimport { renderToString } from '@vue\u002Fserver-renderer'\nimport { createAttaform, escapeForInlineScript, renderAttaformState } from 'attaform'\nimport App from '.\u002FApp.vue'\n\nexport async function render(url: string) {\n  const app = createSSRApp(App)\n  app.use(createAttaform())\n\n  const html = await renderToString(app)\n\n  const attaformState = renderAttaformState(app)\n  \u002F\u002F escapeForInlineScript keeps `\u003C\u002Fscript>` and U+2028 \u002F U+2029\n  \u002F\u002F separators out of the inline payload so it can't break out of\n  \u002F\u002F the \u003Cscript> tag.\n  const payload = escapeForInlineScript(JSON.stringify(attaformState))\n\n  return { html, payload }\n}\n",[37,204,205,219,231,243,255,262,292,308,325,330,349,354,369,375,381,387,414,419,428],{"__ignoreMap":59},[63,206,207,210,213,216],{"class":65,"line":66},[63,208,209],{"class":76},"import",[63,211,212],{"class":87}," { createSSRApp } ",[63,214,215],{"class":76},"from",[63,217,218],{"class":97}," 'vue'\n",[63,220,221,223,226,228],{"class":65,"line":73},[63,222,209],{"class":76},[63,224,225],{"class":87}," { renderToString } ",[63,227,215],{"class":76},[63,229,230],{"class":97}," '@vue\u002Fserver-renderer'\n",[63,232,233,235,238,240],{"class":65,"line":91},[63,234,209],{"class":76},[63,236,237],{"class":87}," { createAttaform, escapeForInlineScript, renderAttaformState } ",[63,239,215],{"class":76},[63,241,242],{"class":97}," 'attaform'\n",[63,244,245,247,250,252],{"class":65,"line":104},[63,246,209],{"class":76},[63,248,249],{"class":87}," App ",[63,251,215],{"class":76},[63,253,254],{"class":97}," '.\u002FApp.vue'\n",[63,256,258],{"class":65,"line":257},5,[63,259,261],{"emptyLinePlaceholder":260},true,"\n",[63,263,265,267,270,273,276,279,283,286,289],{"class":65,"line":264},6,[63,266,77],{"class":76},[63,268,269],{"class":76}," async",[63,271,272],{"class":76}," function",[63,274,275],{"class":83}," render",[63,277,278],{"class":87},"(",[63,280,282],{"class":281},"s4XuR","url",[63,284,285],{"class":76},":",[63,287,288],{"class":146}," string",[63,290,291],{"class":87},") {\n",[63,293,295,297,300,302,305],{"class":65,"line":294},7,[63,296,143],{"class":76},[63,298,299],{"class":146}," app",[63,301,150],{"class":76},[63,303,304],{"class":83}," createSSRApp",[63,306,307],{"class":87},"(App)\n",[63,309,311,314,317,319,322],{"class":65,"line":310},8,[63,312,313],{"class":87},"  app.",[63,315,316],{"class":83},"use",[63,318,278],{"class":87},[63,320,321],{"class":83},"createAttaform",[63,323,324],{"class":87},"())\n",[63,326,328],{"class":65,"line":327},9,[63,329,261],{"emptyLinePlaceholder":260},[63,331,333,335,338,340,343,346],{"class":65,"line":332},10,[63,334,143],{"class":76},[63,336,337],{"class":146}," html",[63,339,150],{"class":76},[63,341,342],{"class":76}," await",[63,344,345],{"class":83}," renderToString",[63,347,348],{"class":87},"(app)\n",[63,350,352],{"class":65,"line":351},11,[63,353,261],{"emptyLinePlaceholder":260},[63,355,357,359,362,364,367],{"class":65,"line":356},12,[63,358,143],{"class":76},[63,360,361],{"class":146}," attaformState",[63,363,150],{"class":76},[63,365,366],{"class":83}," renderAttaformState",[63,368,348],{"class":87},[63,370,372],{"class":65,"line":371},13,[63,373,374],{"class":69},"  \u002F\u002F escapeForInlineScript keeps `\u003C\u002Fscript>` and U+2028 \u002F U+2029\n",[63,376,378],{"class":65,"line":377},14,[63,379,380],{"class":69},"  \u002F\u002F separators out of the inline payload so it can't break out of\n",[63,382,384],{"class":65,"line":383},15,[63,385,386],{"class":69},"  \u002F\u002F the \u003Cscript> tag.\n",[63,388,390,392,395,397,400,402,405,408,411],{"class":65,"line":389},16,[63,391,143],{"class":76},[63,393,394],{"class":146}," payload",[63,396,150],{"class":76},[63,398,399],{"class":83}," escapeForInlineScript",[63,401,278],{"class":87},[63,403,404],{"class":146},"JSON",[63,406,407],{"class":87},".",[63,409,410],{"class":83},"stringify",[63,412,413],{"class":87},"(attaformState))\n",[63,415,417],{"class":65,"line":416},17,[63,418,261],{"emptyLinePlaceholder":260},[63,420,422,425],{"class":65,"line":421},18,[63,423,424],{"class":76},"  return",[63,426,427],{"class":87}," { html, payload }\n",[63,429,431],{"class":65,"line":430},19,[63,432,433],{"class":87},"}\n",[192,435,437],{"id":436},"server-template","Server template",[54,439,443],{"className":440,"code":441,"language":442,"meta":59,"style":59},"language-html shiki shiki-themes github-light github-dark","\u003Cbody>\n  \u003Cdiv id=\"app\">\u003C!--ssr-outlet-->\u003C\u002Fdiv>\n  \u003Cscript>\n    window.__ATTAFORM_STATE__ = {{{ payload }}};\n  \u003C\u002Fscript>\n  \u003Cscript type=\"module\" src=\"\u002Fsrc\u002Fentry-client.ts\">\u003C\u002Fscript>\n\u003C\u002Fbody>\n","html",[37,444,445,454,482,490,500,509,538],{"__ignoreMap":59},[63,446,447,449,452],{"class":65,"line":66},[63,448,119],{"class":87},[63,450,451],{"class":122},"body",[63,453,138],{"class":87},[63,455,456,459,462,465,467,470,473,476,478,480],{"class":65,"line":73},[63,457,458],{"class":87},"  \u003C",[63,460,461],{"class":122},"div",[63,463,464],{"class":83}," id",[63,466,132],{"class":87},[63,468,469],{"class":97},"\"app\"",[63,471,472],{"class":87},">",[63,474,475],{"class":69},"\u003C!--ssr-outlet-->",[63,477,167],{"class":87},[63,479,461],{"class":122},[63,481,138],{"class":87},[63,483,484,486,488],{"class":65,"line":91},[63,485,458],{"class":87},[63,487,123],{"class":122},[63,489,138],{"class":87},[63,491,492,495,497],{"class":65,"line":104},[63,493,494],{"class":87},"    window.__ATTAFORM_STATE__ ",[63,496,132],{"class":76},[63,498,499],{"class":87}," {{{ payload }}};\n",[63,501,502,505,507],{"class":65,"line":257},[63,503,504],{"class":87},"  \u003C\u002F",[63,506,123],{"class":122},[63,508,138],{"class":87},[63,510,511,513,515,518,520,523,526,528,531,534,536],{"class":65,"line":264},[63,512,458],{"class":87},[63,514,123],{"class":122},[63,516,517],{"class":83}," type",[63,519,132],{"class":87},[63,521,522],{"class":97},"\"module\"",[63,524,525],{"class":83}," src",[63,527,132],{"class":87},[63,529,530],{"class":97},"\"\u002Fsrc\u002Fentry-client.ts\"",[63,532,533],{"class":87},">\u003C\u002F",[63,535,123],{"class":122},[63,537,138],{"class":87},[63,539,540,542,544],{"class":65,"line":294},[63,541,167],{"class":87},[63,543,451],{"class":122},[63,545,138],{"class":87},[192,547,549,550,199],{"id":548},"client-entry-clientts","Client (",[37,551,552],{},"entry-client.ts",[54,554,556],{"className":56,"code":555,"language":58,"meta":59,"style":59},"import { createSSRApp } from 'vue'\nimport { createAttaform, hydrateAttaformState } from 'attaform'\nimport App from '.\u002FApp.vue'\n\nconst app = createSSRApp(App)\napp.use(createAttaform())\n\n\u002F\u002F Replay the server's form state BEFORE mounting — forms read from\n\u002F\u002F the hydration bag during setup.\nconst serialized = (window as any).__ATTAFORM_STATE__\nif (serialized !== undefined) hydrateAttaformState(app, serialized)\n\napp.mount('#app')\n",[37,557,558,568,579,589,593,606,619,623,628,633,654,677,681],{"__ignoreMap":59},[63,559,560,562,564,566],{"class":65,"line":66},[63,561,209],{"class":76},[63,563,212],{"class":87},[63,565,215],{"class":76},[63,567,218],{"class":97},[63,569,570,572,575,577],{"class":65,"line":73},[63,571,209],{"class":76},[63,573,574],{"class":87}," { createAttaform, hydrateAttaformState } ",[63,576,215],{"class":76},[63,578,242],{"class":97},[63,580,581,583,585,587],{"class":65,"line":91},[63,582,209],{"class":76},[63,584,249],{"class":87},[63,586,215],{"class":76},[63,588,254],{"class":97},[63,590,591],{"class":65,"line":104},[63,592,261],{"emptyLinePlaceholder":260},[63,594,595,598,600,602,604],{"class":65,"line":257},[63,596,597],{"class":76},"const",[63,599,299],{"class":146},[63,601,150],{"class":76},[63,603,304],{"class":83},[63,605,307],{"class":87},[63,607,608,611,613,615,617],{"class":65,"line":264},[63,609,610],{"class":87},"app.",[63,612,316],{"class":83},[63,614,278],{"class":87},[63,616,321],{"class":83},[63,618,324],{"class":87},[63,620,621],{"class":65,"line":294},[63,622,261],{"emptyLinePlaceholder":260},[63,624,625],{"class":65,"line":310},[63,626,627],{"class":69},"\u002F\u002F Replay the server's form state BEFORE mounting — forms read from\n",[63,629,630],{"class":65,"line":327},[63,631,632],{"class":69},"\u002F\u002F the hydration bag during setup.\n",[63,634,635,637,640,642,645,648,651],{"class":65,"line":332},[63,636,597],{"class":76},[63,638,639],{"class":146}," serialized",[63,641,150],{"class":76},[63,643,644],{"class":87}," (window ",[63,646,647],{"class":76},"as",[63,649,650],{"class":146}," any",[63,652,653],{"class":87},").__ATTAFORM_STATE__\n",[63,655,656,659,662,665,668,671,674],{"class":65,"line":351},[63,657,658],{"class":76},"if",[63,660,661],{"class":87}," (serialized ",[63,663,664],{"class":76},"!==",[63,666,667],{"class":146}," undefined",[63,669,670],{"class":87},") ",[63,672,673],{"class":83},"hydrateAttaformState",[63,675,676],{"class":87},"(app, serialized)\n",[63,678,679],{"class":65,"line":356},[63,680,261],{"emptyLinePlaceholder":260},[63,682,683,685,688,690,693],{"class":65,"line":371},[63,684,610],{"class":87},[63,686,687],{"class":83},"mount",[63,689,278],{"class":87},[63,691,692],{"class":97},"'#app'",[63,694,695],{"class":87},")\n",[14,697,698,699,701],{},"That's it. Every ",[37,700,51],{}," call on the client resolves to the same\nvalues the server rendered.",[42,703,705],{"id":704},"what-crosses-the-wire","What crosses the wire",[21,707,708,714,720],{},[24,709,710,713],{},[37,711,712],{},"form"," — the current reactive value.",[24,715,716,719],{},[37,717,718],{},"errors"," — every error currently in the store.",[24,721,722,725],{},[37,723,724],{},"fields"," — touched \u002F focused \u002F blurred \u002F isConnected \u002F updatedAt\nper path.",[42,727,729],{"id":728},"common-issues","Common issues",[14,731,732],{},[27,733,734],{},"\"The form is empty on the client even though the server rendered values.\"",[21,736,737,752],{},[24,738,739,740,743,744,747,748,751],{},"Did you call ",[37,741,742],{},"hydrateAttaformState(app, payload)"," before\n",[37,745,746],{},"app.mount(...)","? It has to land before ",[37,749,750],{},"setup"," runs.",[24,753,754,755,758,759,762,763,766],{},"Does the form's ",[37,756,757],{},"key"," match between server and client? Hard-code\nit as a string literal. ",[37,760,761],{},"uuidv4()"," or ",[37,764,765],{},"Math.random()"," produces a\nfresh key per render and breaks the match.",[14,768,769],{},[27,770,771],{},"\"Field errors from the server disappear on first interaction.\"",[14,773,774,775,778,779,407],{},"By design. Any mutation re-runs validation, which can replace the\nerrors. To keep server-provided errors around until the user\ndirties the field, gate the display on\n",[37,776,777],{},"form.fields.\u003Cpath>.touched"," or on ",[37,780,781],{},"form.meta.isDirty",[14,783,784],{},[27,785,786],{},"\"Some fields look right, others don't.\"",[14,788,789,790,793,794,796],{},"Forms created in ",[37,791,792],{},"onMounted"," or event handlers aren't in the SSR\nsnapshot. Create forms during ",[37,795,750],{}," so the server sees them.",[42,798,800],{"id":799},"reference-test","Reference test",[14,802,803,804,811],{},"The bare-Vue round-trip has an end-to-end test at\n",[805,806,808],"a",{"href":807},"..\u002F..\u002Ftest\u002Fssr-bare-vue\u002Fround-trip.test.ts",[37,809,810],{},"test\u002Fssr-bare-vue\u002Fround-trip.test.ts","\n— reading it is faster than reconstructing the wiring from scratch.",[813,814,815],"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 .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}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 .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 .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}",{"title":59,"searchDepth":73,"depth":73,"links":817},[818,819,826,827,828],{"id":44,"depth":73,"text":45},{"id":189,"depth":73,"text":190,"children":820},[821,823,824],{"id":194,"depth":91,"text":822},"Server (entry-server.ts)",{"id":436,"depth":91,"text":437},{"id":548,"depth":91,"text":825},"Client (entry-client.ts)",{"id":704,"depth":73,"text":705},{"id":728,"depth":73,"text":729},{"id":799,"depth":73,"text":800},"md",{},"\u002Fdocs\u002Frecipes\u002Fssr-hydration",{"title":5,"description":16},"docs\u002Frecipes\u002Fssr-hydration","qhpoyWFOjKmNEKbW_zcAm_qVJ8XwXoJ6OgQ-Zfh0ab4",1777934136675]