Rearchitecting complex multi-step forms

Quick demo

Introduction

Contra is built to grow, manage and pay your flexible talent network, all in one place. A core feature of the product that enables this is the ability to create proposals for projects. The existing forms used to do this were a battleground of tech debt and legacy code that had become difficult to maintain and extend. We were facing regular bugs that blocked a core value proposition of the product and slowed iteration time nearly to a halt on new features. The existing implementation also left little to no room for integration and unit tests, which made it difficult to catch regressions and ensure the quality of the codebase.

Proposals on Contra are complex, multi-step forms with a significant number of permutations and edge cases. There are 3-4 steps, each step containing a number of sections, fields, and conditional logic. If you can think about how a freelancer or client would like to setup billing for a project, it's likely that our proposal form can accomodate it—ongoing projects (think retainers), fixed price projects, hourly projects, and more. This complexity, coupled with complex and scattered state management, was a breeding ground for bugs and regressions. If you haven't, check out the Loom above to see a walkthrough of the current form UI for some context on the functionality.

I idenfitied the core architecture issues where many of our bugs were coming from, and created a list of technical debt that had been introduced. I set out to rearchitect and rebuild the proposal forms with the goal to implement a simple and scalable solution with type safety, test coverage, and a sturdy user experience.

Example fieldset

proposal form screenshot

Fixing the issues

By proposing a new architecture and presenting an accompanying RFC, I was able to get buy-in from the team and began to work. I started by creating a new form component that would be the foundation of the new proposal. This component was built with a focus on simplicity and reusability. It was designed to be easily extended and customized for different types of proposals.

In order to acheive the goal of type safety and ample test coverage, I decided to implement a very simple, but robust, reducer using React's native reducer and useReducer hook, using Zod schemas as a single source of truth for both type inference and validation. This allowed me to easily test the reducer in isolation and ensure that it was working as expected. Everything was fully typed end-to-end.

Resolution

In the end, every action in the reducer was able to be covered by a test, and the form component was able to be easily extended and customized for different types of proposals. The new proposal forms were also much easier to maintain and extend than the old ones, and the team was able to move much faster on new features and in the year since the new forms were implemented, we have had zero bug reports related to the proposal form state management.

The new proposal form component was a huge success and has been used as a template for other forms in the product. It has also been a great learning experience for the team, and has helped us to avoid many of the pitfalls that we encountered with the old forms. I am proud of the work that I did on this project, and I am excited to see how it will continue to evolve in the future.

Screenshots

While I can't share the codebase or do an in-depth code walk through, I can share some screenshots of the new proposal forms in action, for a sense of the work that was done. Here are a few screenshots of the new proposal forms in action:

code snippet screenshotcode snippet screenshotcode snippet screenshot