Skip to content
// booting portfolio
the engineer's notebookYash.
loading assets000%
Yash.
All writing
ReactFrontendSystem Design

Building a schema-driven form engine in React

5 min read

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.