{"id":9516,"date":"2026-05-01T12:11:13","date_gmt":"2026-05-01T17:11:13","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=9516"},"modified":"2026-05-01T12:11:14","modified_gmt":"2026-05-01T17:11:14","slug":"introducing-tanstack-form","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/introducing-tanstack-form\/","title":{"rendered":"Introducing TanStack Form"},"content":{"rendered":"\n<p>There&#8217;s no shortage of form libraries to help manage the complexity of form handling, particularly in React. In this post, we&#8217;ll look at <a href=\"https:\/\/tanstack.com\/form\/latest\">TanStack Form<\/a>. Like other TanStack libraries, Form takes strong typing and performance seriously. It&#8217;s also detail-oriented and has planned for every imaginable edge case.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Complexity?<\/h2>\n\n\n\n<p>Forms are a notoriously annoying part of React. They seem simple at first: just create some basic state for each input, wire up your controlled inputs, and that&#8217;s that. But of course you&#8217;ll need validation. And you&#8217;ll probably want to add some niceties, like clearing validation errors as a user types into an invalid field. And you&#8217;ll probably not want to dump your entire form into one component, so you&#8217;d just pass around all those state values. Or put them into context. Or you could use uncontrolled form inputs, in which case you don&#8217;t need those state values, but now you&#8217;ll be dealing with raw DOM elements for all your inputs.<\/p>\n\n\n\n<p>Manually managing your own forms always starts simple, but quickly becomes a pain. Let&#8217;s look at how to manage it all with TanStack Form.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Our First Form<\/h2>\n\n\n\n<p>Let&#8217;s jump in. We&#8217;ll build a form to manage a Product of this structure:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">interface<\/span> Product {\n  name: <span class=\"hljs-built_in\">string<\/span>;\n  price: <span class=\"hljs-built_in\">number<\/span> | <span class=\"hljs-built_in\">string<\/span>;\n  added?: <span class=\"hljs-built_in\">Date<\/span>;\n  description: <span class=\"hljs-built_in\">string<\/span>;\n  skuNumber: <span class=\"hljs-built_in\">string<\/span>;\n  metadata: { name: <span class=\"hljs-built_in\">string<\/span>; value: <span class=\"hljs-built_in\">string<\/span> }&#91;];\n}\n\n<span class=\"hljs-keyword\">const<\/span> defaultProduct: Product = {\n  name: <span class=\"hljs-string\">\"\"<\/span>,\n  price: <span class=\"hljs-number\">0<\/span>,\n  added: <span class=\"hljs-literal\">undefined<\/span>,\n  description: <span class=\"hljs-string\">\"\"<\/span>,\n  skuNumber: <span class=\"hljs-string\">\"\"<\/span>,\n  metadata: &#91;],\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>TanStack Form gives us a <code>useForm<\/code> hook for generating our &#8230;<em>form<\/em>.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> form = useForm({\n  defaultValues: defaultProduct,\n\n  onSubmit: <span class=\"hljs-keyword\">async<\/span> ({ value }) =&gt; {\n    <span class=\"hljs-comment\">\/\/ ...<\/span>\n  },\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can render our form.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form<\/span>\n  <span class=\"hljs-attr\">onSubmit<\/span>=<span class=\"hljs-string\">{event<\/span> =&gt;<\/span> {\n    event.preventDefault();\n    event.stopPropagation();\n\n    form.handleSubmit();\n  }}\n&gt;<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">form<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The <code>&lt;form&gt;<\/code> rendered above is the <code>form<\/code> variable we just created from the <code>useForm<\/code> hook call, <em>not<\/em> a generic HTML <code>&lt;form&gt;<\/code>.<\/p>\n\n\n\n<p>Our <code>onSubmit<\/code> handler prevents the native HTML form behavior, and then calls <code>form.handleSubmit()<\/code> which invokes any validation you define, which we&#8217;ll get to, and, if no validation errors are found, invokes the original <code>onSubmit<\/code> callback you passed to the <code>useForm<\/code> hook.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Managing Fields<\/h2>\n\n\n\n<p>Let&#8217;s look at a single field defined inside our form. We&#8217;ll look at the entire <code>Field<\/code>, and then pick it apart.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form.Field<\/span>\n  <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"name\"<\/span>\n  <span class=\"hljs-attr\">validators<\/span>=<span class=\"hljs-string\">{{<\/span>\n    <span class=\"hljs-attr\">onSubmit:<\/span> ({ <span class=\"hljs-attr\">value<\/span> }) =&gt;<\/span> {\n      if (!value) {\n        return \"Name is required\";\n      }\n    },\n  }}\n  children={field =&gt; (\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">htmlFor<\/span>=<span class=\"hljs-string\">{field.name}<\/span>&gt;<\/span>Product Name<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Input<\/span>\n        <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">{field.name}<\/span>\n        <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">{field.name}<\/span>\n        <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{field.state.value}<\/span>\n        <span class=\"hljs-attr\">onBlur<\/span>=<span class=\"hljs-string\">{field.handleBlur}<\/span>\n        <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{event<\/span> =&gt;<\/span> field.handleChange(event.target.value)}\n      \/&gt;\n      {!field.state.meta.isValid &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"valid-text\"<\/span>&gt;<\/span>{field.state.meta.errors.join(\", \")}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>}\n      {field.state.meta.isPristine &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"pristine-text\"<\/span>&gt;<\/span>Pristine<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>}\n      {field.state.meta.isTouched &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"touched-text\"<\/span>&gt;<\/span>Touched<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>}\n      {field.state.meta.isDirty &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"dirty-text\"<\/span>&gt;<\/span>Dirty<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>}\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  )}\n\/&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Let&#8217;s start at the very top. We have to specify <em>which piece<\/em> of data our form field is managing, and that&#8217;s what the <code>name<\/code> prop is for.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">name = <span class=\"hljs-string\">\"name\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>If you&#8217;re used to TanStack libraries, you&#8217;re probably used to incredibly meticulous static typing, and Form is no different.<\/p>\n\n\n\n<p>When we defined our product:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> defaultProduct: Product = {\n  name: <span class=\"hljs-string\">\"\"<\/span>,\n  price: <span class=\"hljs-number\">0<\/span>,\n  added: <span class=\"hljs-literal\">undefined<\/span>,\n  description: <span class=\"hljs-string\">\"\"<\/span>,\n  skuNumber: <span class=\"hljs-string\">\"\"<\/span>,\n  metadata: &#91;],\n};\n\n<span class=\"hljs-comment\">\/\/ and then ...<\/span>\n\nuseForm({\n  defaultValues: defaultProduct,\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The structure of the <code>defaultValues<\/code> we provided became the structure of the data our form now collects, and maintains. This means things like our <code>Field<\/code>&#8216;s <code>name<\/code> prop is statically checked, and therefore even autocompleted.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1006\" height=\"310\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/img1-1.png?resize=1006%2C310&#038;ssl=1\" alt=\"Field name autocomplete\" class=\"wp-image-9522\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/img1-1.png?w=1006&amp;ssl=1 1006w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/img1-1.png?resize=300%2C92&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/img1-1.png?resize=768%2C237&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>Similarly, the&nbsp;<em>value<\/em>&nbsp;associated with any particular form field is also strongly typed, based on those same <code>defaultValues<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Validators<\/h3>\n\n\n\n<p>Moving on to validators, have a look at this part:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">validators={{\n  onSubmit: <span class=\"hljs-function\">(<span class=\"hljs-params\">{ <span class=\"hljs-params\">value<\/span> }<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">if<\/span> (!value) {\n      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">\"Name is required\"<\/span>;\n    }\n  },\n}}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This defines our validation. TanStack Form allows you to specify where validation occurs. I like having these errors show up only after the user tries to submit the form, but you can specify <code>onChange<\/code>, <code>onBlur<\/code>, or even some other more advanced options. See the <a href=\"https:\/\/tanstack.com\/form\/latest\/docs\/framework\/react\/guides\/validation\">docs<\/a> for more info.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Rendering the Actual Form Input<\/h3>\n\n\n\n<p>How do we actually render the form input? TanStack Form is headless; it gives you the state you need, allowing you to render whatever you want. It does this with a classic React pattern that&#8217;s not used quite as often anymore (hooks removed many of its applications), but is no less valuable for use cases exactly like this: render functions.<\/p>\n\n\n\n<p>Some may not know this, but the <code>children<\/code> value passed into a React component does not have to be a React Node: you can also pass a <em>function<\/em> that returns your React node. That&#8217;s what this is:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">children={<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">field<\/span><\/span>) =&gt;<\/span> (\n  &lt;div&gt;\n    &lt;Label htmlFor={field.name}&gt;Product Name&lt;<span class=\"hljs-regexp\">\/Label&gt;\n    &lt;Input\n      id={field.name}\n      name={field.name}\n      value={field.state.value}\n      onBlur={field.handleBlur}\n      onChange={(event) =&gt; field.handleChange(event.target.value)}\n    \/<\/span>&gt;\n    {!field.state.meta.isValid &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"valid-text\"<\/span>&gt;{field.state.meta.errors.join(<span class=\"hljs-string\">\", \"<\/span>)}&lt;<span class=\"hljs-regexp\">\/p&gt;}\n    {field.state.meta.isPristine &amp;&amp; &lt;p className=\"pristine-text\"&gt;Pristine&lt;\/<\/span>p&gt;}\n    {field.state.meta.isTouched &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"touched-text\"<\/span>&gt;Touched&lt;<span class=\"hljs-regexp\">\/p&gt;}\n    {field.state.meta.isDirty &amp;&amp; &lt;p className=\"dirty-text\"&gt;Dirty&lt;\/<\/span>p&gt;}\n  &lt;<span class=\"hljs-regexp\">\/div&gt;\n)}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"learn-more\">You don&#8217;t <em>have<\/em> to use the <code>children<\/code> prop; you can also pass this function as the actual value in between <code>&lt;form.Field&gt;<\/code> and <code>&lt;\/form.Field&gt;<\/code>. The two are equivalent. The TanStack Form docs use the <code>children<\/code> prop, but you can use whichever you prefer; they&#8217;re identical.<\/p>\n\n\n\n<p>TanStack Form&#8217;s <code>Field<\/code> component handles the grunt work of <em>calling<\/em> the function you provide, and it <em>passes<\/em> this function a parameter that has everything we need to render everything.<\/p>\n\n\n\n<p>In this code, I&#8217;m rendering a ShadCN <code>Label<\/code>, and <code>Input<\/code>. The field prop passed to my render function gives me a name value, plus a state object that has things like the current value. Naturally, there&#8217;s an <code>onChange<\/code> handler we need to invoke with any updated values, but you might wonder why I need to pass an <code>onBlur<\/code> handler. That&#8217;s to help some of the field&#8217;s state. In the code above, you can see the validation error info attached to the field&#8217;s <code>state.meta<\/code> object, but there&#8217;s also input state like <code>isTouched<\/code> and <code>isDirty<\/code>. Check the <a href=\"https:\/\/tanstack.com\/form\/latest\/docs\/framework\/react\/guides\/basic-concepts#field-state\">the docs<\/a> for a full accounting of all these various state values, but <code>isTouched<\/code> indicates whether the user has ever focused-and-blurred your input, and the <code>onBlur<\/code> callback is what makes this work.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Array Fields<\/h2>\n\n\n\n<p>Our original data had a metadata field that was an Array.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">interface<\/span> Product {\n  <span class=\"hljs-comment\">\/\/ ...<\/span>\n  metadata: { name: <span class=\"hljs-built_in\">string<\/span>; value: <span class=\"hljs-built_in\">string<\/span> }&#91;];\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Let&#8217;s see how TanStack Form manages that. First, we use a <code>Field<\/code> as we have been, but we set its mode to &#8220;array.&#8221; The &#8220;field&#8221; in the render prop will have a <code>pushValue<\/code> method for adding an item to the array, as well as a <code>removeValue<\/code> method for removing one of the items by index.<\/p>\n\n\n\n<p>From there, <code>field.state.value<\/code> inside the <code>Field<\/code> component&#8217;s render function would be the array itself. We can loop it, and for each item, render <em>another<\/em> field for each item.<\/p>\n\n\n\n<p>Let&#8217;s look at the code.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"Vala\" data-shcb-language-slug=\"vala\"><span><code class=\"hljs language-vala shcb-code-table\"><span class='shcb-loc'><span>&lt;form.Field name=<span class=\"hljs-string\">\"metadata\"<\/span> mode=<span class=\"hljs-string\">\"array\"<\/span>&gt;\n<\/span><\/span><span class='shcb-loc'><span>  {field =&gt; (\n<\/span><\/span><span class='shcb-loc'><span>    &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>      &lt;Button variant=<span class=\"hljs-string\">\"outline\"<\/span> type=<span class=\"hljs-string\">\"button\"<\/span> onClick={() =&gt; field.pushValue({ name: <span class=\"hljs-string\">\"\"<\/span>, value: <span class=\"hljs-string\">\"\"<\/span> })}&gt;\n<\/span><\/span><span class='shcb-loc'><span>        Add Metadata\n<\/span><\/span><span class='shcb-loc'><span>      &lt;\/Button&gt;\n<\/span><\/span><span class='shcb-loc'><span>      {field.state.value.map((_, idx) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>        <span class=\"hljs-keyword\">return<\/span> (\n<\/span><\/span><span class='shcb-loc'><span>          &lt;div key={idx}&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>              &lt;form.Field\n<\/span><\/span><mark class='shcb-loc'><span>                name={`metadata&#91;${idx}].name`}\n<\/span><\/mark><span class='shcb-loc'><span>                validators={{\n<\/span><\/span><span class='shcb-loc'><span>                  onSubmit: ({ value }) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>                    <span class=\"hljs-keyword\">if<\/span> (!value) {\n<\/span><\/span><span class='shcb-loc'><span>                      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">\"Name is required\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>                    }\n<\/span><\/span><span class='shcb-loc'><span>                  },\n<\/span><\/span><span class='shcb-loc'><span>                }}\n<\/span><\/span><span class='shcb-loc'><span>                children={field =&gt; (\n<\/span><\/span><span class='shcb-loc'><span>                  &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    &lt;Label htmlFor={field.name}&gt;Name&lt;\/Label&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    &lt;Input\n<\/span><\/span><span class='shcb-loc'><span>                      id={field.name}\n<\/span><\/span><span class='shcb-loc'><span>                      name={field.name}\n<\/span><\/span><span class='shcb-loc'><span>                      value={field.state.value}\n<\/span><\/span><span class='shcb-loc'><span>                      onBlur={field.handleBlur}\n<\/span><\/span><span class='shcb-loc'><span>                      onChange={event =&gt; field.handleChange(event.target.value)}\n<\/span><\/span><span class='shcb-loc'><span>                      placeholder=<span class=\"hljs-string\">\"\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>                    \/&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    {!field.state.meta.isValid &amp;&amp; &lt;p <span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span>=\"<span class=\"hljs-title\">text<\/span>-<span class=\"hljs-title\">error<\/span>\"&gt;<\/span>{field.state.meta.errors.join(<span class=\"hljs-string\">\", \"<\/span>)}&lt;\/p&gt;}\n<\/span><\/span><span class='shcb-loc'><span>                  &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>                )}\n<\/span><\/span><span class='shcb-loc'><span>              \/&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>              &lt;form.Field\n<\/span><\/span><span class='shcb-loc'><span>                name={`metadata&#91;${idx}].value`}\n<\/span><\/span><span class='shcb-loc'><span>                validators={{\n<\/span><\/span><span class='shcb-loc'><span>                  onSubmit: ({ value }) =&gt; {\n<\/span><\/span><span class='shcb-loc'><span>                    <span class=\"hljs-keyword\">if<\/span> (!value) {\n<\/span><\/span><span class='shcb-loc'><span>                      <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-string\">\"Value is required\"<\/span>;\n<\/span><\/span><span class='shcb-loc'><span>                    }\n<\/span><\/span><span class='shcb-loc'><span>                  },\n<\/span><\/span><span class='shcb-loc'><span>                }}\n<\/span><\/span><span class='shcb-loc'><span>                children={field =&gt; (\n<\/span><\/span><span class='shcb-loc'><span>                  &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    &lt;Label htmlFor={field.name}&gt;Value&lt;\/Label&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    &lt;Input\n<\/span><\/span><span class='shcb-loc'><span>                      id={field.name}\n<\/span><\/span><span class='shcb-loc'><span>                      name={field.name}\n<\/span><\/span><span class='shcb-loc'><span>                      value={field.state.value}\n<\/span><\/span><span class='shcb-loc'><span>                      onBlur={field.handleBlur}\n<\/span><\/span><span class='shcb-loc'><span>                      onChange={event =&gt; field.handleChange(event.target.value)}\n<\/span><\/span><span class='shcb-loc'><span>                      placeholder=<span class=\"hljs-string\">\"\"<\/span>\n<\/span><\/span><span class='shcb-loc'><span>                    \/&gt;\n<\/span><\/span><span class='shcb-loc'><span>                    {!field.state.meta.isValid &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"text-error\"<\/span>&gt;{field.state.meta.errors.join(<span class=\"hljs-string\">\", \"<\/span>)}&lt;\/p&gt;}\n<\/span><\/span><span class='shcb-loc'><span>                  &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>                )}\n<\/span><\/span><span class='shcb-loc'><span>              \/&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;div&gt;\n<\/span><\/span><span class='shcb-loc'><span>              &lt;Button type=<span class=\"hljs-string\">\"button\"<\/span> onClick={() =&gt; field.removeValue(idx)}&gt;\n<\/span><\/span><span class='shcb-loc'><span>                Remove\n<\/span><\/span><span class='shcb-loc'><span>              &lt;\/Button&gt;\n<\/span><\/span><span class='shcb-loc'><span>            &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>          &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>        );\n<\/span><\/span><span class='shcb-loc'><span>      })}\n<\/span><\/span><span class='shcb-loc'><span>    &lt;\/div&gt;\n<\/span><\/span><span class='shcb-loc'><span>  )}\n<\/span><\/span><span class='shcb-loc'><span>&lt;\/form.Field&gt;\n<\/span><\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">Vala<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">vala<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Notice the <code>name<\/code> on the inner field.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">name={<span class=\"hljs-string\">`metadata&#91;<span class=\"hljs-subst\">${idx}<\/span>].name`<\/span>}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>TanStack Form allows, and even type checks, that this is a perfectly valid name.<\/p>\n\n\n\n<p>We can add items to our metadata.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;Button\n  <span class=\"hljs-keyword\">type<\/span>=<span class=\"hljs-string\">\"button\"<\/span>\n  onClick={<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> field.pushValue({ name: <span class=\"hljs-string\">\"\"<\/span>, value: <span class=\"hljs-string\">\"\"<\/span> })}&gt;\n    Add Metadata\n&lt;<span class=\"hljs-regexp\">\/Button&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>As well as remove them.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;Button\n  <span class=\"hljs-keyword\">type<\/span>=<span class=\"hljs-string\">\"button\"<\/span>\n  onClick={<span class=\"hljs-function\"><span class=\"hljs-params\">()<\/span> =&gt;<\/span> field.removeValue(idx)}&gt;\n  Remove\n&lt;<span class=\"hljs-regexp\">\/Button&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Referencing Other Field Values<\/h2>\n\n\n\n<p>Let&#8217;s get a little contrived and pretend that, when entering a product, if the price is &gt; 50, we require a description. Let&#8217;s further pretend that whenever <code>price<\/code> has a value &gt; 50, we immediately want to display a helpful message indicating that a description will be required since the price is what it is.<\/p>\n\n\n\n<p>The naive solution won&#8217;t work; we can&#8217;t just do this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> DescriptionFieldUseStore: FC&lt;{ form: ProductForm }&gt; = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">props<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { form } = props;\n\n  <span class=\"hljs-keyword\">const<\/span> price = form.getFieldValue(<span class=\"hljs-string\">\"price\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> descriptionRequired = <span class=\"hljs-keyword\">typeof<\/span> price === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; price &gt; <span class=\"hljs-number\">50<\/span>;\n\n  <span class=\"hljs-comment\">\/\/ later ...<\/span>\n  {descriptionRequired &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"text-yellow-800\"<\/span>&gt;Description is required when price is greater than $<span class=\"hljs-number\">50<\/span>&lt;<span class=\"hljs-regexp\">\/p&gt;}\n}<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The reason is that <code>form.getFieldValue(\"price\");<\/code> is not reactive. This is for performance reasons. If you want to dynamically and reactively get access to other parts of the form, you have a few options.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><code>useStore<\/code><\/h3>\n\n\n\n<p>The <code>useStore<\/code> hook is one option.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { useStore } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-form\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows you to grab whatever you need reactively.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> price = useStore(form.store, <span class=\"hljs-function\"><span class=\"hljs-params\">state<\/span> =&gt;<\/span> state.values.price);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\"><code>Subscribe<\/code><\/h3>\n\n\n\n<p>The other option is the <code>Subscribe<\/code> component. You specify the slice of the form&#8217;s state you want, and you&#8217;re given a render function with that reactive slice of the form passed in<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\">&lt;form.Subscribe selector={<span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">formState<\/span><\/span>) =&gt;<\/span> ({ price: formState.values.price })}&gt;\n  {<span class=\"hljs-function\">(<span class=\"hljs-params\">{ <span class=\"hljs-params\">price<\/span> }<\/span>) =&gt;<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> descriptionRequired = <span class=\"hljs-keyword\">typeof<\/span> price === <span class=\"hljs-string\">\"number\"<\/span> &amp;&amp; price &gt; <span class=\"hljs-number\">50<\/span>;\n    <span class=\"hljs-keyword\">return<\/span> (\n      &lt;form.Field\n        name=<span class=\"hljs-string\">\"description\"<\/span>\n        <span class=\"hljs-comment\">\/\/ and so on...<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Use whichever is more convenient for your particular use case.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Composition<\/h2>\n\n\n\n<p>Do we have everything we need? Not really. Our <code>form<\/code> object was created from the <code>useForm<\/code> hook, and we&#8217;ve been using that for our <code>Field<\/code> components. <code>Field<\/code> is not a component we import; instead, it&#8217;s created on the fly, from the <code>useForm<\/code> hook, and attached to the <code>form<\/code> object returned therefrom. The reason is that all our form fields will be strongly typed, with appropriate names, values, etc.<\/p>\n\n\n\n<p>But we may not want to put our entire form into one big React component if things grow even moderately large. Breaking up our form into smaller components is a great idea, and we could simply pass our <code>form<\/code> object around as needed, as a prop.<\/p>\n\n\n\n<p>But what&#8217;s the <em>type<\/em> of this <code>form<\/code> object? Unfortunately, Typescript reports it as:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> form: ReactFormExtendedApi&lt;Product, FormValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormAsyncValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormAsyncValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormAsyncValidateOrFn&lt;Product&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormValidateOrFn&lt;...&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormAsyncValidateOrFn&lt;...&gt; | <span class=\"hljs-literal\">undefined<\/span>, FormAsyncValidateOrFn&lt;...&gt; | <span class=\"hljs-literal\">undefined<\/span>, unknown&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The return type from the <code>useForm<\/code> type is a generic that takes <strong>a lot<\/strong> of args, and they&#8217;re required. These control things like the data in the form, obviously, but also things like validation.<\/p>\n\n\n\n<p>Fortunately, a good understanding of TypeScript can go a long, long way here. Let&#8217;s move the call to <code>useForm<\/code> into its own function<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> useProductForm = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">onSubmit<\/span>: (<span class=\"hljs-params\">value<\/span>: <span class=\"hljs-params\">Product<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">void<\/span>) =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> useForm({\n    defaultValues: defaultProduct,\n\n    onSubmit: <span class=\"hljs-keyword\">async<\/span> ({ value }) =&gt; {\n      onSubmit(value);\n    },\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can leverage some TypeScript helpers and inferred typing to easily get the type we&#8217;re looking for.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">type<\/span> ProductForm = ReturnType&lt;<span class=\"hljs-keyword\">typeof<\/span> useProductForm&gt;;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And now we can break up our form into smaller components, and pass the <code>form<\/code> object in correctly.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> DescriptionFieldSubscribe: FC&lt;{ form: ProductForm }&gt; = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">props<\/span><\/span>) =&gt;<\/span> {<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Composing Better<\/h2>\n\n\n\n<p>Let&#8217;s imagine this bit of markup.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Label<\/span> <span class=\"hljs-attr\">htmlFor<\/span>=<span class=\"hljs-string\">{field.name}<\/span>&gt;<\/span>Product Name<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">Label<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">Input<\/span>\n    <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">{field.name}<\/span>\n    <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">{field.name}<\/span>\n    <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">{field.state.value}<\/span>\n    <span class=\"hljs-attr\">onBlur<\/span>=<span class=\"hljs-string\">{field.handleBlur}<\/span>\n    <span class=\"hljs-attr\">onChange<\/span>=<span class=\"hljs-string\">{event<\/span> =&gt;<\/span> field.handleChange(event.target.value)}\n  \/&gt;\n  {!field.state.meta.isValid &amp;&amp; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">p<\/span> <span class=\"hljs-attr\">className<\/span>=<span class=\"hljs-string\">\"text-error\"<\/span>&gt;<\/span>{field.state.meta.errors.join(\", \")}<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">p<\/span>&gt;<\/span>}\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Maybe it&#8217;s even more complex than that, and it&#8217;s clear that it would make sense to put into a reusable component.<\/p>\n\n\n\n<p>You have a few options.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The <code>AnyFieldApi<\/code> Type<\/h3>\n\n\n\n<p>There&#8217;s a nice <code>AnyFieldApi<\/code> type exported from TanStack Form. This faithfully represents <em>any<\/em> field object. The only catch is that the value is typed as any. How could it not? It&#8217;s an umbrella type for any field. But in practice, this might be <em>fine<\/em>.<\/p>\n\n\n\n<p>But you can define any components you want, and pass your field in as <code>AnyFieldApi<\/code>, and then just type the <code>value<\/code> prop as needed.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> SimpleTextField: FC&lt;{ label: <span class=\"hljs-built_in\">string<\/span>; field: AnyFieldApi }&gt; = <span class=\"hljs-function\"><span class=\"hljs-params\">props<\/span> =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { label, field } = props;\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div&gt;\n      &lt;Label htmlFor={field.name}&gt;{label}&lt;<span class=\"hljs-regexp\">\/Label&gt;\n      &lt;Input\n        id={field.name}\n        name={field.name}\n        value={field.state.value}\n        onBlur={field.handleBlur}\n        onChange={event =&gt; field.handleChange(event.target.value)}\n      \/<\/span>&gt;\n      {!field.state.meta.isValid &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"text-error\"<\/span>&gt;{field.state.meta.errors.join(<span class=\"hljs-string\">\", \"<\/span>)}&lt;<span class=\"hljs-regexp\">\/p&gt;}\n    &lt;\/<\/span>div&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Then:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form.Field<\/span>\n  <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"skuNumber\"<\/span>\n  <span class=\"hljs-attr\">validators<\/span>=<span class=\"hljs-string\">{{<\/span>\n    <span class=\"hljs-attr\">onSubmit:<\/span> ({ <span class=\"hljs-attr\">value<\/span> }) =&gt;<\/span> {\n      if (!value) {\n        return \"SKU is required\";\n      }\n    },\n  }}\n  children={field =&gt; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">SimpleTextField<\/span> <span class=\"hljs-attr\">label<\/span>=<span class=\"hljs-string\">\"SKU Number\"<\/span> <span class=\"hljs-attr\">field<\/span>=<span class=\"hljs-string\">{field}<\/span> \/&gt;<\/span>}\n\/&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">FieldComponents and useFieldContext<\/h3>\n\n\n\n<p>Really, we could end this post here. Everything we\u2019ve seen will cover the overwhelming majority of any use case imaginable. But Form has some advanced features that are at least worth looking at.<\/p>\n\n\n\n<p>Let&#8217;s start with some new imports.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">import<\/span> { createFormHook, createFormHookContexts } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"@tanstack\/react-form\"<\/span>;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This part is a little weird and won&#8217;t make complete sense just yet, but we&#8217;ll clear it up as we go.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> { fieldContext, useFieldContext, formContext } = createFormHookContexts();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Let&#8217;s now create a reusable form component.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> BasicTextField: FC&lt;{ label: <span class=\"hljs-built_in\">string<\/span> }&gt; = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">props<\/span><\/span>) =&gt;<\/span> {\n  <span class=\"hljs-keyword\">const<\/span> { label } = props;\n  <span class=\"hljs-keyword\">const<\/span> field = useFieldContext&lt;<span class=\"hljs-built_in\">string<\/span>&gt;();\n\n  <span class=\"hljs-keyword\">return<\/span> (\n    &lt;div&gt;\n      &lt;Label htmlFor={field.name}&gt;{label}&lt;<span class=\"hljs-regexp\">\/Label&gt;\n      &lt;Input\n        id={field.name}\n        name={field.name}\n        value={field.state.value}\n        onBlur={field.handleBlur}\n        onChange={(event) =&gt; field.handleChange(event.target.value)}\n      \/<\/span>&gt;\n      {!field.state.meta.isValid &amp;&amp; &lt;p className=<span class=\"hljs-string\">\"text-error\"<\/span>&gt;{field.state.meta.errors.join(<span class=\"hljs-string\">\", \"<\/span>)}&lt;<span class=\"hljs-regexp\">\/p&gt;}\n    &lt;\/<\/span>div&gt;\n  );\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It&#8217;s just a simple component, which takes a <code>label<\/code> as a prop. But notice there&#8217;s no <code>field<\/code> prop; instead, we have this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> field = useFieldContext&lt;<span class=\"hljs-built_in\">string<\/span>&gt;();<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This says, &#8220;grab whatever the current field is, in this form.&#8221; And since we can&#8217;t rely on inferred typing, since we don&#8217;t have direct access to the type, we have to pass a generic argument to let TypeScript know that this is, in fact, a string field.<\/p>\n\n\n\n<p>Now we can tell TanStack about our custom form component and get back a new hook to create our form with.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"TypeScript\" data-shcb-language-slug=\"typescript\"><span><code class=\"hljs language-typescript\"><span class=\"hljs-keyword\">const<\/span> { useAppForm } = createFormHook({\n  fieldContext,\n  formContext,\n  fieldComponents: { BasicTextField },\n  formComponents: {},\n});\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">const<\/span> useProductForm = <span class=\"hljs-function\">(<span class=\"hljs-params\"><span class=\"hljs-params\">onSubmit<\/span>: (<span class=\"hljs-params\">value<\/span>: <span class=\"hljs-params\">Product<\/span><\/span>) =&gt;<\/span> <span class=\"hljs-built_in\">void<\/span>) =&gt; {\n  <span class=\"hljs-keyword\">return<\/span> useAppForm({\n    defaultValues: defaultProduct,\n\n    onSubmit: <span class=\"hljs-keyword\">async<\/span> ({ value }) =&gt; {\n      onSubmit(value);\n    },\n  });\n};<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">TypeScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">typescript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can do everything as before, but when we provide the markup for a field, we have a new option.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">form.AppField<\/span>\n  <span class=\"hljs-attr\">name<\/span>=<span class=\"hljs-string\">\"name\"<\/span>\n  <span class=\"hljs-attr\">validators<\/span>=<span class=\"hljs-string\">{{<\/span>\n    <span class=\"hljs-attr\">onSubmit:<\/span> ({ <span class=\"hljs-attr\">value<\/span> }) =&gt;<\/span> {\n      if (!value) {\n        return \"Product name is required!\";\n      }\n    },\n  }}\n  children={(field) =&gt; <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">field.BasicTextField<\/span> <span class=\"hljs-attr\">label<\/span>=<span class=\"hljs-string\">\"Product Name\"<\/span> \/&gt;<\/span>}\n\/&gt;<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This allows us to attach any custom components directly to our form, which can then access whatever field you&#8217;re currently editing.<\/p>\n\n\n\n<p>Form also supports reusing groups of components at the form level. For example, if you had a call to <code>&lt;form.Subscribe&gt;<\/code> and wanted to reuse that entire structure, there are utilities for that (<code>formComponents<\/code>). It&#8217;s a variation on the theme we already saw, so check <a href=\"https:\/\/tanstack.com\/form\/latest\/docs\/framework\/react\/guides\/form-composition\">the docs<\/a> if you&#8217;re curious.<\/p>\n\n\n\n<p>For extremely large applications, these features can come in handy and help keep everything organized.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Concluding Thoughts<\/h2>\n\n\n\n<p>TanStack Form is a surprisingly pleasant form library. The API is a bit more superficially complex than you might expect, but once you understand how it works, you immediately see its power, and flexibility.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>TanStack Form offers a powerful solution for handling form complexity in React. It emphasizes strong typing, performance, and detail management.<\/p>\n","protected":false},"author":21,"featured_media":9519,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[82,59,3,62,240],"class_list":["post-9516","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-form-validation","tag-forms","tag-javascript","tag-react","tag-tanstack"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2026\/04\/tanstackform.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9516","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/21"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=9516"}],"version-history":[{"count":18,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9516\/revisions"}],"predecessor-version":[{"id":9543,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/9516\/revisions\/9543"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/9519"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=9516"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=9516"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=9516"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}