Building a schema-driven form engine in React
When I built a resume builder, the naïve version had a hand-written form component per section — experience, education, skills, projects… Ten of them, each subtly different, each a place for bugs. The second version describes the form as data and renders it generically.
Describe, don't hard-code
Every section becomes a schema. The engine reads the schema; it doesn't know or care what "experience" is.
const experienceSchema = {
key: "experience",
label: "Experience",
repeatable: true,
fields: [
{ name: "company", type: "text", required: true },
{ name: "role", type: "text", required: true },
{ name: "period", type: "text" },
{ name: "bullets", type: "list", itemType: "textarea" },
],
};
Adding a new section type is now a data change, not a component.
One generic renderer
function Field({ field, value, onChange }) {
switch (field.type) {
case "text":
return <input value={value ?? ""} onChange={(e) => onChange(e.target.value)} />;
case "textarea":
return <textarea value={value ?? ""} onChange={(e) => onChange(e.target.value)} />;
case "list":
return <ListField field={field} value={value ?? []} onChange={onChange} />;
default:
return null;
}
}
Validation rides along on the schema too (required, max, regex), so the rules live in one place and the renderer stays dumb.
Live preview without re-rendering the world
The form and the preview read the same store. The trick is making sure typing in one field doesn't re-render every other field. Keep state normalized and select narrowly:
// each field subscribes only to its own slice
const value = useResume((s) => s.data[sectionKey]?.[index]?.[field.name]);
With granular selectors, a keystroke updates exactly one input and the preview — nothing else. That's what kept editing smooth even with a deeply nested document.
Why it was worth it
- Extensibility: new templates and sections are configuration.
- Consistency: one validation path, one render path — fewer edge cases.
- Export stays simple: because the document is plain data, generating an ATS-friendly PDF with jsPDF is just walking the schema.
Schema-first feels like extra work on day one. By the third section type, it's the only thing that scales.
Comments (0)
- Be the first to comment.