project: typed-react-form
Updated Mon Feb 22 2021 Stijn Rogiest

A fast, completely type-checked React form state manager. Supports nested objects/arrays.

Star me! CodeStix/typed-react-formLoading...

React using javascript is great, but for larger projects, it can get quite tedious when refactoring code. For example, when renaming a property, vscode (for example) does not always replace every occurrence, which can lead to frustrating search-and-destroy bug scenarios.

Fortunately, there is typescript. When set up correctly, typescript will whine (typescript: 😠) about variables and functions that you try to access but don't exist. This makes maintaining a big project so much easier because it lets you know where things do not seem right.

✔️ Basic example js vs ts

Why?

Recently, I had to build an application for a client, which contained a lot of large forms. A few examples of forms I had to create:

  • A form to create questionnaires, which is a list of questions, which must be able to contain more than 30 questions. For each list item, 5 inputs would be created. 😨
  • A form to create a post.
  • A form to create an event.
  • A form to register and login.
  • The list keeps on going...

Great! Let's use a form management library. I had used Formik and react-hook-form before, they were fine and I used Formik for a while.

Because there were a lot of big forms, I had to refactor these a lot, when renaming a field for example. Unfortunately, Formik does not warn you if forget to rename an input name. This required me to manually double-check inputs when renaming fields to see if its field name was set up correctly.

renaming problem

This was the main problem I had with these form management libraries. They are great for React with javascript, but with typescript, it would be nice to have a type-checked solution.

Secondly, Formik was too slow for the questionnaire form (which contains a list of 50+ inputs), it caused everything to rerender each time. (Not the case with react-hook-form) 😕

I looked for a decent type-checked React form manager, but couldn't find any. After all of this, I decided to write my own!

Development

When starting this project, there were a few things I wanted to make sure to include:

  • Type checking!
  • Fast dynamic array fields
  • Validation using yup
  • Keeping it simple

Challenge 1: Type checking & nested fields

A nested field is a field inside an object. When using Formik, you use a dot to specify nested fields:

✔️ Using Formik with nested fields

// `user` is an object
<Formik initialValues={{ email: "john@example.com", user: { name: "John" } }}>
    {() => (
        <Form>
            <Field name="email" />
            {/* Using a dot */}
            <Field name="user.name" />
        </Form>
    )}
</Formik>

Unfortunately, there is no way to type-check this, typescript cannot know that "user.name" (as a string) means the "name" field in the "user" field. Typescript can only type-check field names in the upper-most object.

keyof test

The solution is child forms! Like a normal form is based on an object containing initial values, a child form is a form based on an object field in another form.

✔️ Using typed-react-form with nested fields

// `user` is an object
const form = useForm({ email: "john@example.com", user: { name: "John" } });
// The "user" string is type checked and is a key of form.values
const userForm = useChildForm(form, "user");
return (
    <form>
        <FormInput form={form} name="email" />
        {/* The "name" string is type checked and is a key of userForm.values */}
        <FormInput form={userForm} name="name" />
    </form>
);

Typescript knows that values in userForm come from the value of the "user" field in form, its parent.

This way, type-checking is possible:

type checking

Great! Problem 1 is solved! 😀

Challenge 2: Minimal rerenders & fast array fields

Most of the form libraries rerender the whole form when something changes, in my case, this is a problem, because I have array fields containing 50+ items, which would all be rerendered and cause lagging.

The solution is to only rerender the things that have changed. This works by using listeners; hooks/components that listen for state changes on a specific (or all) fields of a form. Listener hooks behave a lot like useState.

The built-in inputs like <FormInput /> use this hook and only rerender itself when needed. The useForm hook, for example, does not cause a rerender when something changes, it only creates a form state manager, which others can listen to.

array rendering

Red flashes indicate what parts of the form are being re-rendered.

As you can see, Formik rerenders the whole array each time something changes, while typed-react-form only rerenders the array when its size changes. Awesome! 😮

Final thoughts

I wrote this post because I'm pretty sure I'm not the only one that would be interested in a completely type-checked solution like this.

Check out the documented project here.

I would definitely like some feedback on the documentation. This is the first time writing such an extended documentation and I have to say, it is hard to write easy to understand documentation for others when you know every detail of the project yourself.

Anyway, I hope this was interesting for some of you. Have a great day and thanks for reading! 😄