TanStack
Guides

Async Initial Values

Forms often edit data loaded from an API. A server-state library such as TanStack Query should own fetching, caching, loading, and retries; TanStack Form should own the editable copy.

There are two supported ways to provide async initial values: wait to create the form until the data is ready, or create it immediately with complete fallback values. Choose the approach that best matches the loading experience your UI needs.

Wait for data before creating the editor

Render a loading or error state first, then mount a component whose defaultValues are complete.

tsx
import { useQuery } from '@tanstack/react-query'
import { useForm } from '@tanstack/react-form'

type User = {
  firstName: string
  lastName: string
}

export function UserPage({ userId }: { userId: string }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((res) => res.json()),
  })

  if (userQuery.isPending) return <p>Loading…</p>
  if (userQuery.isError) return <p role="alert">Could not load the user.</p>

  return <UserEditor key={userId} user={userQuery.data} />
}

function UserEditor({ user }: { user: User }) {
  const form = useForm({
    defaultValues: user,
    onSubmit: async ({ value }) => {
      await fetch('/api/users', {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(value),
      })
    },
  })

  return <form>{/* fields */}</form>
}

This pattern is useful when no form state is needed during loading. Mounting the editor only after data exists also makes it explicit when switching records should create a fresh form.

Create the form before data arrives

You can instead call useForm in the same component as useQuery. Provide a static, shape-complete fallback so defaultValues are never undefined.

tsx
const emptyUser: User = {
  firstName: '',
  lastName: '',
}

export function UserPage({ userId }: { userId: string }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then((res) => res.json()),
  })

  const form = useForm({
    defaultValues: userQuery.data ?? emptyUser,
    onSubmit: async ({ value }) => {
      await fetch('/api/users', {
        method: 'PUT',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify(value),
      })
    },
  })

  if (userQuery.isPending) return <p>Loading…</p>
  if (userQuery.isError) return <p role="alert">Could not load the user.</p>

  return <form>{/* fields */}</form>
}

When the query data replaces the fallback, TanStack Form applies the new defaultValues to untouched fields while preserving fields the user has already touched. In this example, the fields remain unmounted during loading, so the user cannot edit them before the loaded values arrive. You can also render the empty form immediately when that better suits the UI.

This is the approach demonstrated by the repository's examples/react/query-integration example.

Background refetches and record changes

defaultValues establish the form's baseline; they are not a fully controlled values prop. In the fallback pattern, later defaultValues updates use the same touched-field-preserving behavior.

If background refetches arrive while a user is editing, decide at the product level whether to keep local edits, prompt before replacing them, or call form.reset(nextValues) deliberately. Do not overwrite in-progress input merely because a query refreshed.

When switching to a different record, either remount a keyed editor as in the first pattern or deliberately reset the existing form. Otherwise, touched values from the previous record can be preserved when the new defaults arrive.