T
TanStack2mo ago
blank-aquamarine

Validation not triggering correctly for conditionally rendered fields

Quick note upfront: I know this is a “me” problem, not a TanStack Form issue 😄 — but I could use some help figuring it out. I’m running into a validation issue with conditionally rendered fields. For example, I have a radio button that toggles between “Yes” and “No.” When “Yes” is selected, additional form fields are shown — but those fields don’t trigger validation until I click somewhere on the screen or focus a field. The same thing happens when I dynamically add fields to a field array. The validation does happen, but only after I interact with the form again (like clicking or focusing somewhere). Here’s a simplified version of my code for reference. Happy to share more details if needed. Really appreciate any guidance! Just to clarify the UX I’m aiming for: - On blur or submit: Errors should appear when a user blurs a field or submits the form (validating all fields). - While editing: If a user starts typing in a field that previously had an error, the error should disappear while they’re editing — and only reappear if the field is still invalid after blur. - Dynamic fields: When new fields are rendered conditionally (e.g. based on a selection or added to a field array), they should follow the same validation rules as the rest of the form — without needing an extra click or interaction to trigger validation. ^ Everything works as expected, except for the dynamic field portion… Composed text field component:
import TextField from "@custom-library/textfield";
import { useStore } from "@tanstack/react-form";
import React from "react";
import { useFieldContext, useFormContext } from "../../../../hooks/form";

export const FormTextField = ({
...textFieldProps
}: Omit<
React.ComponentProps<typeof TextField>,
"name" | "onChange" | "value" | "onBlur"
>) => {
const field = useFieldContext<string>();
const form = useFormContext();

const isValid = useStore(field.store, (state) => state.meta.isValid);
const isBlurred = useStore(field.store, (state) => state.meta.isBlurred);
const fieldError = useStore(
field.store,
(state) => state.meta.errors?.[0]?.message,
);
const submissionAttempts = useStore(
field.form.store,
(state) => state.submissionAttempts,
);

const displayError =
(isBlurred || submissionAttempts > 0) && !isValid ? fieldError : undefined;

return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (

<TextField
{...textFieldProps}
name={field.name}
value={field.state.value || ""}
onChange={(event) => field.handleChange(event.target.value)}
onBlur={field.handleBlur}
disabled={isSubmitting || textFieldProps.disabled}
error={displayError}
/>
)}
</form.Subscribe>
);
};
import TextField from "@custom-library/textfield";
import { useStore } from "@tanstack/react-form";
import React from "react";
import { useFieldContext, useFormContext } from "../../../../hooks/form";

export const FormTextField = ({
...textFieldProps
}: Omit<
React.ComponentProps<typeof TextField>,
"name" | "onChange" | "value" | "onBlur"
>) => {
const field = useFieldContext<string>();
const form = useFormContext();

const isValid = useStore(field.store, (state) => state.meta.isValid);
const isBlurred = useStore(field.store, (state) => state.meta.isBlurred);
const fieldError = useStore(
field.store,
(state) => state.meta.errors?.[0]?.message,
);
const submissionAttempts = useStore(
field.form.store,
(state) => state.submissionAttempts,
);

const displayError =
(isBlurred || submissionAttempts > 0) && !isValid ? fieldError : undefined;

return (
<form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (

<TextField
{...textFieldProps}
name={field.name}
value={field.state.value || ""}
onChange={(event) => field.handleChange(event.target.value)}
onBlur={field.handleBlur}
disabled={isSubmitting || textFieldProps.disabled}
error={displayError}
/>
)}
</form.Subscribe>
);
};
Form component (I’m using Zod): 


export const ExampleForm = () => {
const form = useAppForm({
defaultValues,
validators: {
onBlur: myZodSchema,
},

listeners: {
onChange: ({ fieldApi }) => {
if (fieldApi.state.meta.errors.length > 0) {
fieldApi.setMeta((prev) => ({
...prev,
errorMap: {},
}));
}
},
},

onSubmit: async ({ value }) => {
console.log("🔥 Form submitted:", value);
},
});
export const ExampleForm = () => {
const form = useAppForm({
defaultValues,
validators: {
onBlur: myZodSchema,
},

listeners: {
onChange: ({ fieldApi }) => {
if (fieldApi.state.meta.errors.length > 0) {
fieldApi.setMeta((prev) => ({
...prev,
errorMap: {},
}));
}
},
},

onSubmit: async ({ value }) => {
console.log("🔥 Form submitted:", value);
},
});
2 Replies
stormy-gold
stormy-gold2mo ago
this may be an issue. I don't have much time at the moment and couldn't find the related GH issue, but I'll summarize that it is essentially a newly rendered field will validate on form level before it exists, causing desync that requires one rerender to trigger. Would that apply to this scenario too?
blank-aquamarine
blank-aquamarineOP2mo ago
@Luca | LeCarbonator that sounds about right, yes.

Did you find this page helpful?