Building type-safe forms in React

Marton Bodonyi
Behind the genes at Eugene
3 min readFeb 26, 2021

--

At Eugene, our web front-end’s are built using a combo of React, TypeScript and GraphQL.

We’ve been wrestling with form libraries at Eugene since we started pumping out front-end tools in our first project, the Genie report generator. Up until recently, it’s felt like the benefits of using a form library have been outweighed by the downside of having to wrestle to make them behave the way we want them to. That is, until the latest project, where we had the opportunity to take stock and try some new tooling…

Our short legacy of Form libraries

We’re only a young company but up until our latest project we’d implemented two different form libraries in our different products. Our internal patient-management-system, Genie, uses Formik and our consumer facing forms have used react-json-schema-form. The DX on both is pretty frustrating and neither fill me with joy whenever I need to build a new form or widget. If you’re using the word ‘wrestle’ as an analogy for coding then you know something is up with the tooling or libraries you’re using.

Outlining our requirements

First things first, on our web front-ends we use React. That ain’t about to change anytime soon. Our second requirement is that we need to easily be able to implement third party widgets like react-phone-number-input and react-select. It needs to work in Gatsby. It needs to be Typescript friendly. It needs to support front-end validation. It needs to play nice with GraphQL. It needs to treat hooks and contexts in React as first-class citizens (no render methods! Sorry Formik). And finally, and importantly, it needs to be easy to write tests for.

And the winner is … combo of yup & react-hook-form 🚀

Yup

Yup’s typescript documentation has a pretty good run down on how to define a schema that connects to typescript. Because we use react-i18next for localising error message we can’t use types derived automatically from our yup schema because our schemas are memoized at render time based on the current language dictionary. Instead, we define form submission data in separate interfaces and then use yup.schemaOf to ensure our submission data types and validation schema match.

Combining yup schema’s with react-i18next.

react-hook-form

No surprises about react-hook-form treating contexts and hooks as first class citizens. For our forms we created a base form component that uses FormProvider context and the useForm hook and will even throw type errors if we forget to add validation for one of our form fields (or we misspell a form field name).

The awesome thing about using FormProvider is that none of the form library logic needs to be passed down to our widgets so our widgets props can be dedicated to presentation rather than form functionality.

Once those base pieces are together, making a form that validates and submits kit registration numbers in the format of IB12345678 is as easy as…

Writing tests

Using react-hook-form made it really hard to use Enzyme to do selector based tests but in doing that it introduced us to the creatively named ‘Testing Library’ which has rocked our world ever since. One of the great things about Testing Library is that it forces us to create accessible forms by using roles and high quality labels to select elements instead of running assertions on react nodes and rendered components. Traditionally you might select an input on the screen using a css selector but with Testing Library you use the field label instead. Here’s an example of a test for the form above.

Interested in being a part of a team that builds type-safe, validating, unit-tested forms? Head over to https://angel.co/company/eugenelabs and get in contact with us.

--

--