A tiny, type‑safe utility that turns form elements, objects, and schema into a fully typed form system with parsing and update helpers.
Without this package, wiring up a form with multiple fields means juggling individual useState calls and manual onChange handlers for each one:
function ProfileForm() {
const [name, setName] = useState("");
const [age, setAge] = useState(0);
const [role, setRole] = useState("viewer");
const handleSubmit = () => {
console.log({ name, age, role });
};
return (
<form onSubmit={handleSubmit}>
<label>
Name
<input
type="text"
value={name}
onChange={e => setName(e.target.value)}
/>
</label>
<label>
Age
<input
type="number"
value={age}
onChange={e => setAge(Number(e.target.value))}
/>
</label>
<label>
Role
<select value={role} onChange={e => setRole(e.target.value)}>
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
</label>
<button type="submit">Save</button>
</form>
);
}This gets tedious fast — every field needs its own state, its own handler, and manual type coercion.
This library is designed for developers who want strongly typed, schema‑driven form state without adopting a full form framework. It’s a great fit if you:
- Prefer type‑safe form state that updates automatically as your schema changes
- Want to avoid writing one useState per field or manually parsing values from inputs
- Like the idea of defining your form once and getting helpers, parsing, and state management for free
- Need a tiny, dependency‑free utility that plays nicely with React but doesn’t lock you into any framework
- Want predictable, explicit control over your form logic without magic or hidden behavior
Run npm install @presidenttree94/form-utils in the terminal.
useForm combines state management and element binding into a single call. You define a schema once, and each field gets a value and a setValue that handles parsing for you.
import { useForm } from "@presidenttree94/form-utils";
function ProfileForm() {
const { form, elements, reset } = useForm(
{ name: "", age: 0, role: "viewer" },
{
name: { label: "Name" }, // "text" type is default
age: { label: "Age", type: "number" },
role: { label: "Role", options: ["viewer", "editor", "admin"], defaultOption: "Select a role" },
}
);
const handleSubmit = () => {
console.log(form); // { name: string, age: number, role: string }
};
return (
<form onSubmit={handleSubmit}>
<label>
{elements.name.label}
<input
type={elements.name.type}
value={elements.name.value}
onChange={e => elements.name.setValue(e.target.value)}
/>
</label>
<label>
{elements.age.label}
<input
type={elements.age.type}
value={elements.age.value}
onChange={e => elements.age.setValue(e.target.value)}
/>
</label>
<label>
{elements.role.label}
<select
value={elements.role.value}
onChange={e => elements.role.setValue(e.target.value)}
>
<option disabled value="">{elements.role.defaultOption}</option>
{elements.role.options.map(o => (
<option key={o} value={o}>{o}</option>
))}
</select>
</label>
<button type="submit">Save</button>
<button type="button" onClick={reset}>Reset</button>
</form>
);
}age is automatically parsed as a number via the type: "number" config — no manual Number() calls needed.
useForm is a convenience wrapper, but you can use the two underlying pieces independently. This is useful when you want to share form state across components, process data before showing the user, or derive elements at a different point in your render tree.
import { useFormState, buildFormElements } from "@presidenttree94/form-utils";
function ProfileForm() {
// Step 1: manage state
const { form, update, updateMany, reset } = useFormState({
name: "",
age: 0,
role: "viewer",
});
// Step 2: build elements when needed
const elements = buildFormElements(form, update, {
name: { label: "Name" },
age: { label: "Age", type: "number" },
role: { label: "Role", options: ["viewer", "editor", "admin"], defaultOption: "Select a role" },
});
// Bulk-update multiple fields at once (e.g. pre-filling from an API response)
const prefill = () => updateMany({ name: "Alice", age: 30 });
const handleSubmit = () => {
console.log(form);
};
return (
<form onSubmit={handleSubmit}>
{Object.entries(elements).map(([key, field]) => (
field.options ?
<label>
{field.label}
<select
value={field.value}
onChange={e => field.setValue(e.target.value)}
>
<option disabled value="">{field.defaultOption}</option>
{field.options.map(o => (
<option key={o} value={o}>{o}</option>
))}
</select>
</label>
:
<label>
{field.label}
<input
type={field.type}
value={field.value}
onChange={e => field.setValue(e.target.value)}
/>
</label>
))}
<button type="submit">Save</button>
<button type="button" onClick={prefill}>Pre-fill</button>
<button type="button" onClick={reset}>Reset</button>
</form>
);
}Combines useFormState and buildFormElements. Returns { form, elements, update, updateMany, reset }.
Manages form state. Returns:
form— current valuesupdate(key, value)— update a single fieldupdateMany(partial)— update multiple fields at oncereset()— restore initial values
Builds a map of form elements from the current state, update function, and schema. Each element extends its FieldConfig with:
value— the current field valuesetValue(raw)— parses the raw string input and callsupdate
| Property | Type | Description |
|---|---|---|
label |
string |
Display label for the field |
type |
string |
Input type ("text", "number", etc.) |
required |
boolean |
Whether the field is required |
options |
Option[] |
Available options for select fields |
multi |
boolean |
Whether multiple selections are allowed |
defaultOption |
string |
Placeholder text for the default option |
parse |
(raw: string | string[]) => Value |
Custom parser, overrides the built-in type logic |