The Challenge: Dynamic Form Architecture for Government Norwegian municipalities administer hundreds of different grant programs, each with unique application requirements. Building a separate form for each program was not scalable. The solution needed to be a 'form builder' that enable government employees to construct application forms dynamically without writing code.
One technical challenge was creating a hybrid UI that handled both static system fields (applicant name, organization number - fields that appear on every form) and dynamic user-generated fields (custom questions specific to a grant program). These two types of fields needed to coexist seamlessly, sharing validation logic and submission flows.
Further complicating matters: the forms needed to support conditional logic. For example, 'If the applicant selects Yes for question 3, show question 4; otherwise, hide it.' This required a rule engine that could evaluate field dependencies in real-time as users typed.
Google Forms-Inspired Builder Interface My designer said 'it should be like Google Forms', so I tried to reverse-engineer the UX of Google Forms. The main thing to replicate was the 'click-to-edit' pattern: clicking on a field makes it editable; clicking outside auto-saves and returns to preview mode. This great UX feels natural but is complex to implement.
The problem: How does the component know whether a click is 'inside' or 'outside' when the 'inside' area changes dynamically as fields are added? I implemented a ref-based solution with a custom hook. Each editable field registers its DOM node with a context, and a global click listener checks whether the click target is a descendant of any registered node.
Auto-save added another layer of complexity. I used a debounced save function that waits 500ms after the last keystroke before persisting to the server. This prevents API spam while ensuring changes are never lost. I also implemented optimistic UI updates - the form immediately reflects changes while the save is in progress, showing a subtle 'Saving...' indicator that disappears on success.
Validation was tricky because both static and dynamic fields needed validation. I implemented my own validation schema system that merges static field rules (e.g., 'Name is required') with dynamic rules defined in the form configuration (e.g., 'Question 5 must be a number between 1-100').
Conditional Logic & Rule Engine To support conditional fields ('Show field B only if field A equals X'), I built a lightweight rule engine. Rules are stored as JSON objects with a simple structure: `{ field: 'question_3', operator: 'equals', value: 'Yes', action: 'show', targets: ['question_4'] }`.
The form renderer subscribes to field changes via React Hook Form's `watch()` API. When a watched field changes, the rule engine evaluates all rules that reference that field, updating the visibility state of dependent fields. This required careful optimization - evaluating hundreds of rules on every keystroke could tank performance.
I optimized by building a dependency graph at form initialization. This graph maps each field to the rules that depend on it, allowing the engine to only evaluate relevant rules when a field changes. For a form with 50 fields and 20 rules, this reduced evaluation time from ~15ms to <1ms per keystroke.
Component Library & Design System To prevent UI inconsistency and reduce code duplication across the two applications, I established a component library. Every reusable pattern - buttons, inputs, modals, dropdowns - became a component in a shared npm package. This ensured visual consistency, improved maintainability, and accelerated development velocity for the rest of the team.
Both applications, but especially the admin interface, includes many data tables for viewing grant applications, tracking approval workflows, generating reports, and much more. I standardized these using React Table (now TanStack Table), creating a reusable `Table` component with built-in filtering, sorting, pagination, row selection and a context menu. The table received standard React Table props, allowing any dataset to be plugged in easily.
Architecture Patterns: Separating Concerns I established a strict architecture pattern to prevent the codebase from becoming spaghetti: top-level 'container' components handle data fetching via React Query, while 'presentation' components receive data via props and focus solely on rendering.
For example, the `ApplicationListPage` container fetches data using `useApplications()`, handles loading/error states, and passes the data to `ApplicationListTable`, a pure presentation component.
React Context API was heavily used for state specific to a given route, with each page having its own context provider. This was an attempt to avoid prop drilling and keep UI components strictly presentational, though I would not implement an architecture like this again.