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.
Render a loading or error state first, then mount a component whose defaultValues are complete.
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.
You can instead call useForm in the same component as useQuery. Provide a static, shape-complete fallback so defaultValues are never undefined.
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.
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.