· tanstack-form · 5 min read

TanStack Form: Arrays & Dynamic Fields

Handle arrays with a dynamic number of fields in TanStack Form on a React project.

Handle arrays with a dynamic number of fields in TanStack Form on a React project.

Your form might not have a fixed number of fields, that’s where arrays come in! Let’s see today how TanStack Form behaves in this scenario.

This is Chapter 3 of the TanStack Form series, talking about this new Form library which supports React, Angular, Vue, Solid, and Lit.

We’ll learn how to setup an array field and how to add, remove, and even move elements around. This works in both flavors with arrays of primitives (strings, numbers, etc) and objects with nested fields.

TanStack Logo
Learn More About TanStack

Discover all 21 tutorials I've created on Router and other TanStack libraries for web developers

Browse Tutorials

The video version for this chapter is available here:

Code is as usual on GitHub at the 03-dynamic-arrays branch (leave a star if you like it! ⭐️).

The final result will look like this:

Final result

Array of strings

Let’s start from an array of primates, in our case strings. Here are some snippets of what we need to do:

1. Define the form schema

const form = useForm({
    defaultValues: {
      ...
      interests: [] as string[],
      ...
    });

2. Add the field to the form

This takes a few steps, let’s see them one by one. First of all, you need to create a Field component with mode="array" as a container where all elements will have their own components.

<form.Field
  name="interests"
  mode="array"
  children={(field) => (
      // your fields go here
  )}
/>

From here, field.state.value is an array and we can cycle through it with the map function to render each element.

field.state.value.map((_, index) => (
  <form.Field
    name={`interests[${index}]`}
    children={(subField) => (
      <Input
        type="text"
        value={subField.state.value}
        autoFocus
        onChange={(e) => subField.handleChange(e.target.value)}
      />
    )}
  />
));

Notice how the first parameter of the map function is not used, we just need the index to create the correct name for the field and we’ll use subField to control the input.

Side note, the autoFocus attribute is used to focus the input as soon as it’s added. We’ll get there in a moment.

3. Add the button to add a new element

You might want to add elements dynamically, for example with a button. The easiest way is to use the field object provided in the parent Field component and call field.pushValue('') from a button.

<form.Field
  name="interests"
  mode="array"
  children={(field) => (
    <>
      {field.state.value.map((_, index) =>
        // your components here (the snippet above)
      )}
      <Button type="button" variant={'outline'} onClick={() => field.pushValue('')}>
        Add
      </Button>
    </>
  )}
/>

4. Add the button to remove an element

Lastly, you might want to remove elements. This time you’ll need the index of the element you want to remove, that’s why the easiest place to put the button is inside the map function using field.removeValue(index).

<form.Field
  name="interests"
  mode="array"
  children={(field) =>
    field.state.value.map((_, index) => (
      <div key={index}>
        ...
        <Button variant={'destructive'} onClick={() => field.removeValue(index)}>
          <X />
        </Button>
      </div>
    ))
  }
/>

5. Full example

Now, by assembling all the pieces of the puzzle, your array field with add/remove and single-element handling will be similar to this:

<form.Field
  name="interests"
  mode="array"
  children={(field) => (
    <>
      <Label className="mr-2">Interests</Label>
      {field.state.value.map((_, index) => (
        <div key={index} className="flex gap-2 my-2">
          <form.Field
            name={`interests[${index}]`}
            children={(subField) => (
              <Input
                type="text"
                value={subField.state.value}
                autoFocus
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          />
          <Button variant={'destructive'} onClick={() => field.removeValue(index)}>
            <X />
          </Button>
        </div>
      ))}
      <Button type="button" variant={'outline'} onClick={() => field.pushValue('')}>
        Add
      </Button>
    </>
  )}
/>

Moving elements

There’s still one tiny detail we can implement, what if we want to let the user sort the elements? A cool interaction could be via drag & drop but let’s keep it simple for now as we want to explore the API.

We can add a dropdown to move the element at a given index. The component in the example is a Select from shadcn but all that really matters here is the field.moveValue(index, newIndex) function.

Once again the full example, the only difference is the addition of the Select component inside the map function to display it on each row.

<form.Field
  name="interests"
  mode="array"
  children={(field) => (
    <>
      <Label className="mr-2">Interests</Label>
      {field.state.value.map((_, index) => (
        <div key={index} className="flex gap-2 my-2">
          <Select value={`${index}`} onValueChange={(newIndex) => field.moveValue(index, +newIndex)}>
            <SelectTrigger className="w-28">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {field.state.value.map((_, index) => (
                <SelectItem key={index} value={`${index}`}>
                  # {index + 1}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <form.Field
            name={`interests[${index}]`}
            children={(subField) => (
              <Input
                type="text"
                value={subField.state.value}
                autoFocus
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          />
          <Button variant={'destructive'} onClick={() => field.removeValue(index)}>
            <X />
          </Button>
        </div>
      ))}
      <Button type="button" variant={'outline'} onClick={() => field.pushValue('')}>
        Add
      </Button>
    </>
  )}
/>

Array of objects

Similarly to the array of strings, you can have an array of objects.

const form = useForm({
    defaultValues: {
      ...
      skills: [] as { language: string; rating: number }[],
      ...
    });

This time the subfields will no longer be primitives but objects with their own fields. The main difference is that while interest[0] is a valid name since it’s a primitive, with the object you’ll find that skills[0] does not work as it’s not a value you can access directly.

The values are nested inside the object, so you’ll need to use skills[0].language and skills[0].rating to access them.

In short, your field.state.value.map will render two form.Field components, one for each field of the object.

Here’s the full example with also the add/remove buttons:

<form.Field
  name="skills"
  mode="array"
  children={(field) => (
    <>
      <Label className="mr-2">Skills</Label>
      {field.state.value.map((_, index) => (
        <div key={index} className="flex gap-2 my-2">
          <form.Field
            name={`skills[${index}].language`}
            children={(subField) => (
              <Input
                type="text"
                value={subField.state.value}
                autoFocus
                onChange={(e) => subField.handleChange(e.target.value)}
              />
            )}
          />
          <form.Field
            name={`skills[${index}].rating`}
            children={(subField) => (
              <Input
                type="number"
                value={subField.state.value}
                onChange={(e) => subField.handleChange(e.target.valueAsNumber)}
              />
            )}
          />
          <Button variant={'destructive'} onClick={() => field.removeValue(index)}>
            <X />
          </Button>
        </div>
      ))}
      <Button type="button" variant={'outline'} onClick={() => field.pushValue({ language: '', rating: 0 })}>
        Add
      </Button>
    </>
  )}
/>

Conclusion

And that’s it for Chapter 3! If you want to learn more about arrays in TanStack Form, check out the official documentation.

In case you missed the previous chapters, you can find the YouTube playlist here: TanStack Form series.

The code is on the 03-dynamic-arrays branch.

Interested in more content about TanStack Form? Let me know in the comments below if you have any questions or suggestions for the next chapters!

About the author
Leonardo

Hello! My name is Leonardo and as you might have noticed, I like to talk about Web Development and Open Source!

I use GitHub every day and my favourite editor is Visual Studio Code... this might influence a little bit my conent! :D

If you like what I do, you should have a look at my YouTube Channel!

tanstack-form (3 Parts Series)
You might also like
Back to Blog