React Form - Part 2

In the previous blog post I showed you a way I developed to pass around form fields and let the communication flow from the parent to the child by using objects and modify the internals of those objects. In this short blog post, I want to expand on this approach and show how I used it to model the entire form. There are still some problems and I am not fully satisfied with the current version, but I think it works well enough to act as a starting point.

The main idea is that I try to model the actual data structure that I visualize. A form consists of multiple fields, so it makes sense to put this in a data structure. I feel that with modern technology and languages, we have forgotten about proper modelling of the system. Instead we would typically create helper functions in some utils file to do all the work instead. In a next blog post I will try to advocate to reconsider this approach, and I am by no means innocent as you will see in this example.

The idea to have a form object is on one hand to group everything together and make it easier to work with, but on the other hand it also makes state management in React easier. Just imagine having to keep track of all those fields separately, especially since most of them will need to be updated together (this is due to the approach where fields are checked and updated when a user presses the submit button).

In the end the code would look like this:

export type Form = {
    [name: string]: FormField<any>;
};


export default function LoginSection(): JSX.Element {
    const [form, setForm] = useState<Form>({
        email: { value: "", required: true },
        password: { value: "", required: true }
    });

const submit = useCallback((): Promise<void> => {
        form.email.error = !isEmailValid(form.email.value);
        form.password.error = isEmpty(form.password.value);

        const hasError = Object.values(form).some(
            field => field.error === true
        );
        if (hasError) {
            setForm({ ...form });
            return Promise.resolve();
        }

        ...
    }, [form]);
}

The any type of the FormField is currently a limitation, and I believe I can fix this by having specialized FormField types and use FormField as a union between those fields. The logic in the submit is something that I would also prefer to have encapsulated in the form itself, but this means I would have to convert the Form in a real class, which is something I will investigate as well.

Whether or not I will be able to implement advanced checking logic in the fields or form is a different topic. You may pass a function that can be executed to check the field, but if this function depends on other fields, it can get ugly very quickly. Imagine you have a password and a password repeat field, one of the requirements for the password repeat would be that it matches the password. While this is not difficult, it can become a problem if we create copies of the fields. When we do that, the function will refer to the old version of the password field instead of the new one. Currently there is no such logic and only the entire form is being duplicated to force a re-render.

I think this is an interesting approach that still lets you have a lot of control over everything, but it can for sure be improved. I haven't used this that much yet to see the need to do so, nor have I had the time to investigate some alternatives or improvements