Dirty fields
When a user edits a form, Formisch tracks which fields have changed since they were initialized. The library exposes three methods for working with that dirty state: getDirtyInput, getDirtyPaths, and pickDirty. This guide explains what each one is for, when to reach for it, and why pickDirty exists alongside the others.
What "dirty" means
A field is dirty when its current input differs from its start input. The form starts clean. As the user types, only the fields they touch flip to dirty. Resetting a field (or resetting the form with a new initialInput) clears the dirty flag.
The dirty flag is per-field. Editing a deeply nested value does not flip its ancestors' flags. The dirty-extraction methods walk the tree internally to find dirty descendants, which is why all three methods do the same kind of work under the hood.
The three methods
getDirtyInput
Returns the dirty subtree of the form's input. Use it when you want to ship the values the user typed — typically for a PATCH endpoint that accepts the same shape as your form input.
import {
createForm,
Form,
getDirtyInput,
type SubmitHandler,
} from '@formisch/solid';
import * as v from 'valibot';
const UserSchema = v.object({
name: v.pipe(v.string(), v.nonEmpty()),
email: v.pipe(v.string(), v.email()),
});
export default function EditProfile(props) {
const profileForm = createForm({
schema: UserSchema,
initialInput: props.user,
});
const onSubmit: SubmitHandler<typeof UserSchema> = async () => {
const dirty = getDirtyInput(profileForm);
if (dirty) {
await api.patchUser(props.user.id, dirty);
}
};
return (
<Form of={profileForm} onSubmit={onSubmit}>
{/* fields */}
</Form>
);
}If only email was edited, dirty is { email: 'new@example.com' }. If nothing was edited, it is undefined.
Because dirty state is bound to the form input, getDirtyInput always returns the raw user input. Schema transformations such as v.trim() or v.toNumber() are not applied to the returned values. When you need the validated and transformed output instead, reach for pickDirty (see below).
getDirtyPaths
Returns a list of paths to dirty fields. Use it for logging, telemetry, or driving a custom walker over the form's dirty state.
import { getDirtyPaths } from '@formisch/solid';
const dirtyPaths = getDirtyPaths(profileForm);
// e.g. [['email'], ['user', 'name']]Arrays are atomic: only the array's own path is returned, never the paths of individual items.
pickDirty
Filters a user-supplied value down to its dirty parts using the form's dirty mask. Use it when you have a separately-derived value — most commonly the validated and transformed output from a submit handler — and you want only its dirty parts.
import { pickDirty, type SubmitHandler } from '@formisch/solid';
const onSubmit: SubmitHandler<typeof UserSchema> = async (output) => {
const dirty = pickDirty(profileForm, { from: output });
if (dirty) {
await api.update(dirty);
}
};Why pickDirty exists
Dirty state is tracked against the form input — the raw values the user typed. But Valibot schemas can transform that input into a different output shape before it reaches your submit handler:
import * as v from 'valibot';
const Schema = v.object({
name: v.pipe(v.string(), v.trim()),
age: v.pipe(v.string(), v.toNumber()),
});After validation, output.name has whitespace trimmed and output.age is a number. getDirtyInput would give you the raw strings, because the dirty state is bound to the form input — not to the validated output.
pickDirty(form, { from: output }) is the bridge: it walks the form's dirty tree as a structural mask and reads the corresponding values from the output you supply. The result preserves the transformed types.
import { pickDirty, type SubmitHandler } from '@formisch/solid';
const onSubmit: SubmitHandler<typeof Schema> = async (output) => {
// output.age is a number — pickDirty preserves that.
const dirty = pickDirty(form, { from: output });
if (dirty) {
await api.update(dirty);
}
};If the schema reshapes the output entirely — for example, by combining several input fields into a single output value — pickDirty returns undefined because the shape no longer aligns with the form's input shape.
Atomic arrays
All three methods treat arrays as atomic. When any descendant of an array is dirty, the full array is returned (or its own path, for getDirtyPaths). The methods never produce sparse arrays.
Sparse arrays don't round-trip safely. Serializers compact them, JSON.stringify writes null for holes, and indices lose positional meaning. Returning the full current array preserves order and lets the server treat the array as a complete replacement.
This differs from objects, where clean keys are omitted. Objects model keyed dictionaries; arrays model ordered sequences.
Common patterns
The snippets below assume form is your form store (the result of createForm).
Skip submission when nothing changed
The form's isDirty flag is the cheapest way to ask "is anything dirty?" — it short-circuits on the first dirty field it finds and doesn't allocate. Use it when all you need is the yes/no answer. If you actually consume the output of getDirtyInput, getDirtyPaths, or pickDirty, call that method directly — its return value already signals "nothing dirty", so checking form.isDirty first would just walk the tree twice.
import type { SubmitHandler } from '@formisch/solid';
const onSubmit: SubmitHandler<typeof Schema> = async (output) => {
if (!form.isDirty) {
return;
}
await api.update(output);
};Send only the dirty raw input
import { getDirtyInput } from '@formisch/solid';
const dirty = getDirtyInput(form);
if (dirty) {
await api.patch(dirty);
}Send only the dirty validated output
import { pickDirty, type SubmitHandler } from '@formisch/solid';
const onSubmit: SubmitHandler<typeof Schema> = async (output) => {
const dirty = pickDirty(form, { from: output });
if (dirty) {
await api.update(dirty);
}
};Performance
All three methods walk the form's field tree and call getFieldBool recursively to skip clean subtrees. Cost is effectively linear in field count for typical balanced forms (shallow and wide) and degrades toward O(N²) for deeply nested trees with few siblings at each level.
Call these methods from submit or blur handlers — not from tight reactive loops on every keystroke. For very large or deeply nested forms (thousands of fields), profile before binding them to high-frequency events.