Repository navigation
ConditionalField
The ConditionalField component declaratively shows or hides — and optionally resets — a field or group of fields based on the runtime value of another form field. It integrates directly with TanStack Form and requires no additional store or wiring beyond the form instance already used on the page.
Source: src/components/Form/ConditionalField/
-
Declarative conditions: One condition or an array of conditions can be supplied. Arrays are combined with configurable
AND/ORlogic. -
Nine comparison operators:
equals,not-equals,contains,greater-than,less-than,truthy,falsy,in,not-in. -
Selective re-render: Uses
useSyncExternalStorewith a JSON-serialised snapshot so only changes to the watched fields trigger a re-render. Unrelated form mutations are ignored. -
Field reset on hide (
unregisterOnHide): Callsform.resetFieldfor each name infieldNameswhen the group transitions from visible → hidden. Keeps form state clean. -
Keep-mounted mode (
keepMounted): Children stay in the DOM but are visually hidden with CSS (visibility: hidden,height: 0,opacity: 0). Field state is preserved in TanStack Form. -
CSS transition (
animateIn): Applies agrid-template-rows+opacitytransition when the group becomes visible. The grid technique (0fr → 1fr) is used instead ofmax-heightbecausemax-height: 0 → autois not animatable by CSS. -
TypeScript generics:
TFormDatais inferred from theformprop so conditionfieldnames are not validated at compile time, but theFormInstanceinterface ensures the shape is always compatible without requiring casts.
| Name | Type | Default | Description |
|---|---|---|---|
form |
FormInstance<TFormData> |
— | The TanStack Form instance returned by useForm. |
conditions |
Condition | Condition[] |
— | One condition or an array of conditions that control visibility. |
logic |
'AND' | 'OR' |
'AND' |
How multiple conditions are combined. AND requires every condition to pass. |
children |
ReactNode |
— | Content to show or hide. |
keepMounted |
boolean |
false |
When true, children remain in the DOM but are visually hidden. Preserves TanStack Form state. |
animateIn |
boolean |
true |
Applies a CSS grid-template-rows + opacity transition when the group becomes visible. |
unregisterOnHide |
boolean |
true |
When true, resets fieldNames fields to their defaults when the group becomes hidden. |
fieldNames |
Array<DeepKeys<TFormData>> |
[] |
Names of fields to reset on hide when unregisterOnHide is true. |
interface Condition {
field: string; // top-level key or dot-separated nested path (e.g. 'address.city')
operator: ConditionOperator; // comparison to apply
value?: unknown; // required for all operators except 'truthy' / 'falsy'
}| Operator | Semantics |
|---|---|
equals |
Strict equality (===) |
not-equals |
Strict inequality (!==) |
contains |
String.includes or Array.includes
|
greater-than |
Numeric >
|
less-than |
Numeric <
|
truthy |
Boolean(value) |
falsy |
!value |
in |
Watched value is inside the value array |
not-in |
Watched value is not inside the value array |
import { ConditionalField } from '@/components/Form/ConditionalField';
<form.Field name="hasDistrict">
{(field) => (
<Toggle
id="has-district"
labelText="Assign to district"
toggled={Boolean(field.state.value)}
onToggle={(val) => field.handleChange(val)}
/>
)}
</form.Field>
<ConditionalField
form={form}
conditions={{ field: 'hasDistrict', operator: 'truthy' }}
fieldNames={['districtCode']}
>
<form.Field name="districtCode">
{(field) => (
<ComboBox
id="district-code"
titleText="District"
items={districts}
onChange={({ selectedItem }) => field.handleChange(selectedItem?.code ?? null)}
/>
)}
</form.Field>
</ConditionalField><ConditionalField
form={form}
conditions={[
{ field: 'status', operator: 'equals', value: 'active' },
{ field: 'clientNumber', operator: 'truthy' },
]}
logic="AND"
fieldNames={['samplingCode']}
>
<form.Field name="samplingCode">
{(field) => <TextInput labelText="Sampling Code" value={field.state.value ?? ''} onChange={(e) => field.handleChange(e.target.value)} />}
</form.Field>
</ConditionalField><ConditionalField
form={form}
conditions={[
{ field: 'type', operator: 'equals', value: 'special' },
{ field: 'overrideFlag', operator: 'truthy' },
]}
logic="OR"
>
<p>Special handling required.</p>
</ConditionalField>Use keepMounted when TanStack Form should retain the field's value even when the section is not visible. Children are hidden via CSS rather than being removed from the DOM.
<ConditionalField
form={form}
conditions={{ field: 'isAdvanced', operator: 'truthy' }}
keepMounted
unregisterOnHide={false}
>
<form.Field name="advancedOption">
{(field) => <TextInput labelText="Advanced Option" value={field.state.value ?? ''} onChange={(e) => field.handleChange(e.target.value)} />}
</form.Field>
</ConditionalField><ConditionalField
form={form}
conditions={{ field: 'category', operator: 'in', value: ['A', 'B', 'C'] }}
>
<p>Category A, B, or C specific content.</p>
</ConditionalField>On every form-store change, the hook serialises only the watched field values to a JSON string. Because useSyncExternalStore compares the snapshot with Object.is, a re-render is skipped when the serialised string is unchanged — even if other form values have changed. undefined field values are normalised to null before serialisation so that fields that have never been touched behave identically to explicitly null ones.
Condition field names support dot-separated paths for nested form structures (e.g. 'address.city'). The hook uses an internal getIn(obj, path) helper to traverse the form store's value tree safely. If any segment along the path is missing, null, or not an object, getIn returns undefined, which normalises to null in the snapshot — meaning the condition will not pass until the nested value is set.
When unregisterOnHide is true (the default) and the group transitions from visible → hidden, form.resetField(name) is called once for every name in fieldNames. This removes stale data from the form state before submission, which is the correct behaviour for the majority of conditional-field use cases.
To suppress reset behaviour — for example when the field should retain its value if the user re-shows the section — set unregisterOnHide={false}.
| Behaviour | Default (keepMounted={false}) |
keepMounted={true} |
|---|---|---|
| Hidden DOM state | Children removed from DOM | Children in DOM, aria-hidden="true"
|
| TanStack Form state | Field state cleared on each hide | Field state preserved |
unregisterOnHide |
Resets fields to defaults (if true) |
Resets fields to defaults (if true) |
| Transition | No enter transition (element re-mounts) | Smooth enter/exit transition (CSS) |
-
FormInstance<TFormData>interface — A structural interface is used instead ofReturnType<typeof useForm<TFormData>>because TypeScript cannot check callable constraints on instantiation expressions with free type parameters. The actualReactFormExtendedApireturn type is a structural superset ofFormInstance, so no explicit cast is needed at call sites. -
Memoisation — The inner component is wrapped in
React.memoto avoid re-renders from parent updates that do not change props. -
useSyncExternalStore— The hook subscribes directly to the TanStack Form store with a primitive string snapshot selector. This approach is React 18 concurrent-safe and avoids the tearing issues that arise with manualuseEffect-based subscriptions. -
getInpath resolver — A small exported helper traverses nested objects by dot-separated path string, returningundefined(rather than throwing) when any segment is absent. Used by the snapshot selector to resolve both top-level and nested condition fields. - Condition array stability — The conditions array is JSON-serialised as a dependency so that passing a new array literal on each render does not trigger unnecessary recomputes.
-
fieldnames in conditions are typed asstring, notDeepKeys<TFormData>. TypeScript does not enforce that a condition field actually exists in the form's data shape. Mistyped field names will silently producenullwatched values (behaving asfalsy). Dot-separated paths (e.g.'address.city') are supported and resolved via thegetInhelper. - An empty
conditionsarray ([]) always evaluates tofalseregardless of thelogicprop — a group with no rules is treated as permanently hidden. This is intentional to prevent a vacuous-truth[].every()from showing fields unexpectedly. - The
containsoperator returnsfalsefor non-string, non-array watched values (e.g. numbers). This is intentional to avoid unexpected implicit coercions. -
greater-thanandless-thanoperators require both the watched value and the conditionvalueto be numbers; all other types returnfalse. - CSS transitions on show work only when
keepMounted={true}. The default unmount/remount path re-attaches the element, bypassing thegrid-template-rowsenter transition.
All files achieve 100% statement, branch, function, and line coverage:
-
evaluateCondition— all nine operators covered including string, array, numeric, boolean, and invalid-type edge cases. -
evaluateAll—ANDandORlogic tested against partial and full condition satisfaction; empty array ([]) returnsfalsefor both logic modes. -
getIn— top-level key, multi-segment nested path, missing segment,nullintermediate, and non-object intermediate all covered. - Component integration — visibility toggling,
keepMountedDOM presence,aria-hiddenmanagement,unregisterOnHidereset callbacks,animateInclass toggling, AND/OR multi-condition switching.
- Frontend Architecture Overview — references the form component patterns used in the system.
- Components — Form table must list this component.
- TanStack Form docs
- MultiStepForm component — uses
ConditionalFieldinternally for step-level conditional rendering. - ActiveMultiSelect
- AutoCompleteInput
This wiki documents NR Waste Plus for users and developers. Verify security and deployment guidance against the current application configuration before applying it elsewhere.
- Report an issue or ask a question in the GitHub repository.
- Follow the Contributing Guidelines to propose an update.
- Read the Code of Conduct.
Collection indexes support human browsing and machine retrieval. Keep both current when adding or moving wiki pages.
NR Waste Plus Wiki | GitHub Repository