Lukáš Hudec13.3.2019Back

Handling Forms in React: Research & Custom implementation

Forms are hard...

Let's start with descriptive quotes by well known developers in React community.

“ Forms are hard.”

Jared Palmer - author of Formik

“ Forms are hard. ”

Eric Rasmussen - author of redux-form and final-form

“ React exposed the inherent complexity of form interaction that was always there. ”

Jordan Walke - React & ReasonML core developer

Yes, you should be very careful when it comes to form development. Because form user interface is not just about clicking on some elements. The user usually uses a keyboard, hitting keys several times per second, and the form needs to effectively process those changes and render form data accordingly to those rapid state changes.

And React's unidirectional data flow does not make it easier either. When form values are stored in a central state, every state change leads to calling a render function. So, in larger forms (20+ input fields) you should make sure, that you do not re-render whole form upon each change. Because if do ... you may notice performance issues in the form of lags. And users do not like lags.

Another problem is that there are so many different states the form can actually have.

What you need to know from a form state:

  • What were initial values of fields?
  • What are the current values of fields?
  • What field is focused right now?
  • Which fields were already visited by the user?
  • Which fields contain warnings or validation errors?
  • Is there an asynchronous validation running?
  • Is the form being submitted at the moment?
  • What was the result of form submission?

In this article we will make a research about the most used form libraries for React. Before we move to next parts of this series, which will cover a Formik-based implementation, I want to share my custom implementation of an alternative - a simple and reusable <Form /> component. ✌️

... so what are we waiting for?

What options do we have?

Redux Form

The first popular form handling library from React community was redux-form. Back in the golden era of redux this library was definitely a go to.

redux-form is managing form's state by creating an entry in global redux store (if you do not know, how redux works, check out the docs). This approach however has some tradeoffs.

👍 Pros

  • if you are using redux as a state management library, redux-form is quite easy to integrate
  • form state changes are deterministic & explicit
  • good documentation
  • quite popular among community - 11.3k GitHub ⭐️

👎 Cons

  • inherits redux issues - when your application gets larger, you may notice performance issues - each change creates a new application state, so you need to deal with component updating logic accordingly
  • you have to setup redux to utilize this library
  • quite a large library - 26.7 kB minified + gzipped (v8.1.0)
    • also you need to add React integration: react-redux (4.4 kB minified + gzipped (v6.9.0))

Final Form

final-form was implemented by the author of redux-form - Eric Rasmussen. This time, he has implemented whole library without any dependency and framework agnostic - meaning, you can use it with your favourite framework.

Form state management is based on an Observer pattern. And form field are connected to the form state through subscriptions.

👍 Pros

  • not dependent on any framework - can be used with React, Vue, Angular or plain JavaScript
  • zero configuration
  • very small library - 4.6 kB minified + gzipped (v4.11.0)
  • API is prepared for recently released react-hooks

👎 Cons

  • young (but promising) project - 1.6k GitHub ⭐️

Formik

One of the most popular form library nowadays is Formik.

As the project title says:

Build forms in React, without the tears

Formik is not depending to any state-management library. It handles form state internally. The biggest improvement of this approach is that if you have quite a big application, it does not have a stored form state in the global state store, which may cause performance drops. And since it is not tied with any other state management library you do not have to do a single line of integration configuration. You can just start implementing forms out of the box.

Formik was implemented by Jared Palmer who is an well-known author in React community and has brought to life many React open source libraries.

👍 Pros

  • not dependent on any state management library
  • zero configuration
  • 11.9 kB minified + gzipped (v1.4.2)
  • API is under implementation for recently released react-hooks ( - example)
  • 13.5k GitHub ⭐️
  • good documentation
  • recommended in official React documentation

👎 Cons

  • only for React

Create custom form component

It really depends how complex your forms are going to be. If your forms are simple and mostly share the same event handling logic, instead of copy-pasting handlers implementation you can create a simple self-contained <Form /> component.

Let's define what features is your form component going to handle:

  1. A Controlled form state
  2. Form submission
  3. Tracking touched fields
  4. Tracking submitted state
  5. Ability to set initial values

At first we will design the component API. For better clarification I will use TypeScript and define component interfaces.

To implement reusability you will use, what is called render props technique. How does it work? Basically your <Form /> will accept children prop as a function which will have access to Form handlers and will return a React Element.

interface IFormRenderProps {
    handleSubmit: (event: React.FormEvent<HTMLFormElement>) => any;
    handleChange: (event: React.ChangeEvent<HTMLInputElement>) => any;
    handleBlur: (event: React.FocusEvent<HTMLInputElement>) => any;
    values: { [key: string]: any };
    touched: { [key: string]: boolean };
    isSubmitting: boolean;
}

interface IFormProps {
    onSubmit: Function;
    children: (renderProps: IFormRenderProps) => React.ReactNode;
    initialValues?: { [key: string]: any } | undefined;
}

interface IFormState {
    values: { [key: string]: any };
    touched: { [key: string]: boolean };
    isSubmitting: boolean;
}

Now that you have an understanding of your component behaviour through defined interfaces, you can start with implementation.

At first you will setup the state:

import React from 'react';

// previously defined interfaces

class Form extends React.Component<IFormProps, IFormState> {
    constructor(props: IFormProps) {
        super(props);
        this.state = {
            values: props.initialValues ? { ...props.initialValues } : {},
            touched: {},
            isSubmitting: false
          };
    }

    render() {
        const { children } = this.props;
        const { values, touched, isSubmitting } = this.state;
        return children({
            values,
            touched,
            isSubmitting,
            // for now we will just pass handlers as an empty functions to satisfy TypeScript
            handleSubmit: () => {},
            handleChange: () => {},
            handleBlur: () => {}
        });
   }
}

Now you can test if render props is working:

import Form from './Form';

const Test = () => (
    <Form onSubmit={() => {}}>
        {formRenderProps => {
            console.log(formRenderProps); // { values: {}, touched: {}, isSubmitting: false, ... and handlers }
            return 'Yaay';
        }}
    </Form>
);

So far so good. In the next step, you will implement form handlers:


1. Change handler

By using this method, you will handle change events from form Input elements. To properly update the state without losing previously stored form values, you need to copy values from state and then set the value of the modified field. For this purpose, you can utilize event.target.name where is stored information provided to attribute name in HTML Input element. Also, you need to check if input element is checkbox, and if so, the value is stored in event.target.checked not event.target.value as you would expect.

// inside Form class scope
handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
    this.setState(state => ({
        values: {
            ...state.values,
            [target.name]: target.type === 'checkboxś ? target.checked : target.value
        }
    }));
};

2. Blur handler

This method will handle tracking of the form fields visited by the user. As in Change handler above you will use event.target.name to properly store field information in form state.

// inside Form class scope
handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) => {
    this.setState(state => ({
        touched: {
            ...state.touched,
            [target.name]: true
        }
    }));
};

3. Submit handler

Form submissions are going to be more complex. Most of the time, when you submit form, you make an asynchronous API call. To track whether the form is submitting, you need to set the submitting state before you make an API call and reset it when request is completed even if the request fails. You will implement it with async/await and try-catch-finally JavaScript constructs.

// inside Form class scope
setSubmitting = (isSubmitting: boolean) => {
    this.setState({ isSubmitting });
};

handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    
    const { onSubmit } = this.props;
    const { values } = this.state;
    
    this.setSubmitting(true);
    
    try {
        await onSubmit(values);
    } catch (err) {
        console.error('[Form::handleSubmit] ERROR:', err);
    } finally {
        this.setSubmitting(false);
    }
};

Complete Form component

import React from 'react';

interface IFormRenderProps {
    handleSubmit: (event: React.FormEvent<HTMLFormElement>) => any;
    handleChange: (event: React.ChangeEvent<HTMLInputElement>) => any;
    handleBlur: (event: React.FocusEvent<HTMLInputElement>) => any;
    values: { [key: string]: any };
    touched: { [key: string]: boolean };
    isSubmitting: boolean;
}

interface IFormProps {
    onSubmit: Function;
    children: (renderProps: IFormRenderProps) => React.ReactNode;
    initialValues?: { [key: string]: any } | undefined;
}

interface IFormState {
    values: { [key: string]: any };
    touched: { [key: string]: boolean };
    isSubmitting: boolean;
}

class Form extends React.Component<IFormProps, IFormState> {
    constructor(props: IFormProps) {
        super(props);
        this.state = {
            values: props.initialValues ? { ...props.initialValues } : {},
            touched: {},
            isSubmitting: false
        };
    }

    handleChange = ({ target }: React.ChangeEvent<HTMLInputElement>) => {
        this.setState(state => ({
            values: {
                ...state.values,
                [target.name]:
                    target.type === 'checkbox' ? target.checked : target.value
            }
        }));
    };

    handleBlur = ({ target }: React.FocusEvent<HTMLInputElement>) => {
        this.setState(state => ({
            touched: {
                ...state.touched,
                [target.name]: true
            }
        }));
    };

    handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
        event.preventDefault();
        const { onSubmit } = this.props;
        const { values } = this.state;
        this.setSubmitting(true);
        try {
            await onSubmit(values);
        } catch (err) {
            console.error('[Form::handleSubmit] ERROR:ś, err);
        } finally {
            this.setSubmitting(false);
        }
    };

    setSubmitting = (isSubmitting: boolean) => {
        this.setState({ isSubmitting });
    };

    render() {
        const { children } = this.props;
        const { values, touched, isSubmitting } = this.state;
        return children({
            values,
            touched,
            isSubmitting,
            handleSubmit: this.handleSubmit,
            handleChange: this.handleChange,
            handleBlur: this.handleBlur
        });
    }
}

export default Form;

To check full implementation with a demo app go to my GitHub repository.

You may noticed that I have used a callback or object in setState - read why.

Conclusion

You have just read the first part of series called Handling Forms in React, where I gave you an overview of how to deal with forms in React and what your options are.

Now you should have a fair understanding of why forms are not easy, and why you should be careful implementing them.

I hope that I helped you save some time for researching & reviewing form handling libraries in React.

What comes next...

In the upcoming episode I will tell you about how we are using Formik and Ant design and how to make them work together.

Until then: stay tuned!

Sources

Taming Forms in React - Jared Palmer

Next Generation Forms with React Final Form - Erik Rasmussen

Redux Form Docs

Final Form Docs

Formik Docs

React Forms Docs

Lukáš Hudec

Front-end developer

Send Us a Message