Forms in React, a tale of abstraction and optimisation
Tags: react
Published: 2019-12-12 16:09:00

#Table of contents
 [The basics](#chapter-1)
 [Abstraction](#chapter-2)
 [Optimisation](#chapter-3)
 

In my example I use the [Material-UI](https://material-ui.com/) library, and mostly the [TextField component](https://material-ui.com/components/text-fields/).

It can be removed and adapted to any library or no library at all.

## The basics <a name="chapter-1"></a>

Below is an example of a basic form with a few inputs _(fullWidth is used just for view purposes only)_

```javascript
const Form = () => {
  return (
    <form>
      <TextField label="Name" name="name" type="text" fullWidth />
      <TextField label="Age" name="age" type="number" fullWidth />
      <TextField label="Email" name="email" type="email" fullWidth />
      <TextField label="Password" name="password" type="password" fullWidth />
      <Button type="submit" fullWidth>
        submit
      </Button>
    </form>
  );
}
```

[CodeSandbox example](https://codesandbox.io/s/basic-zw3db)

In order to use the data and do something with it, we would need the following:

##### An object to store the data

For this we will use the `useState` hook from React
```javascript
const [formData, setFormData] = useState({});
```

##### A handler to update the data

- We need a function that takes the `value` and the `name` as a key from the input `event.target` object and updates the `formData` object
```javascript
const updateValues = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
};
```
- Bind the function to the inputs `onChange` event
```javascript
<TextField ... onChange={updateValues} />
```
* *Extra*: Usually in forms there are components have some logic and not update the values via the `event` object and have their own logic, for example an autocomplete component, image gallery with upload and delete, an editor like CKEditor etc. and for this we use another handler
```javascript
const updateValuesWithParams = (name, value) => {
    setFormData({ ...formData, [name]: value });
};
```

##### A handler to submit the data
- The function that does something with the data. In this case it displays it in the `console`.

```javascript
const submitHandler = e => {
    e.preventDefault();

    console.log(formData);
};
```

- Bind the function to the form `onSubmit` event
```javascript
<form onSubmit={submitHandler}>
```

Voila, now we have a form that we can use

[CodeSandbox example](https://codesandbox.io/s/basic-with-handlers-wmnhe)


##Abstraction <a name="chapter-2"></a>

The main idea with abstraction for me is not to have duplicate code or duplicate logic in my components, after that comes abstraction of data layers and so on...

Starting with the code duplication the first thing is to get the `inputs` out into objects and iterate them.

We create an `array` with each field as a separate `object`

```javascript
const inputs = [
  {
    label:'Name',
    name:'name',
    type:'text'
  },
  {
    label:'Age',
    name:'age',
    type:'number'
  },
  {
    label:'Email',
    name:'email',
    type:'email'
  },
  {
    label:'Password',
    name:'password',
    type:'password'
  },
]
```

And just iterate over it in our `form` render

```javascript
const Form = () => {
  ...

  return (
    <form onSubmit={submitHandler}>
      {formFields.map(item => (
        <TextField
          key={item.name}
          onChange={updateValues}
          fullWidth
          {...item}
        />
      ))}
      <Button type="submit" fullWidth>
        submit
      </Button>
    </form>
  );
}
```

[CodeSandbox example](https://codesandbox.io/s/abstraction-of-fields-3zqpm)

So far so good, but what happens if we have more than one form? What happens with the handlers? do we duplicate them also?

My solution was to create a custom hook to handle this. Basically we move the `formData` object and handlers outside the components. 

I ended with a `useFormData` hook

```javascript
import { useState } from "react";

const useFormData = (initialValue = {}) => {
  const [formData, setFormData] = useState(initialValue);

  const updateValues = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
  };

  const updateValuesParams = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
  };

  const api = {
    updateValues,
    updateValuesParams,
    setFormData
  };

  return [formData, api];
};

export default useFormData;
```
Which can be used in our form components as follows
```javascript
const [formData, { updateValues, updateValueParams, setFormData }] = useFormData({});
```
The hook one parameter when called.

- __initialFormData__: An object with initial value for the `formData` state in the hook


The hook returns an array with two values:
- __formData__: The current formData object
- __api__: An object that exposes the handlers outside the hook

Our component now looks like this
```javascript
const Form = () => {
  const [formData, { updateValues }] = useFormData({});

  const submitHandler = e => {
    e.preventDefault();

    console.log(formData);
  };

  return (
    <form onSubmit={submitHandler}>
      {formFields.map(item => (
        <TextField
          key={item.name}
          onChange={updateValues}
          fullWidth
          {...item}
        />
      ))}
      <Button type="submit" fullWidth>
        submit
      </Button>
    </form>
  );
};
```

[CodeSandbox example](https://codesandbox.io/s/abstraction-with-useformdata-hook-ntbmb)

Can we go even further? __YES WE CAN!__

Let's take the example with two forms, what do we have duplicated now?

Well for starters we have the `submitHandler` and the actual `<form>` it self. Working on the `useFormData` hook, we can create a `useForm` hook.

```javascript
import React, { useState } from "react";
import { Button, TextField } from "@material-ui/core";

const useForm = (
  initialFormDataValue = {},
  initalFormProps = {
    fields: [],
    props: {
      fields: {},
      submitButton: {}
    },
    handlers: {
      submit: () => false
    }
  }
) => {
  const [formData, setFormData] = useState(initialFormDataValue);

  const updateValues = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
  };

  const updateValuesParams = ({ target: { name, value } }) => {
    setFormData({ ...formData, [name]: value });
  };

  const formFields = initalFormProps.fields.map(item => (
    <TextField
      key={item.label}
      defaultValue={initialFormDataValue[item.name]}
      onChange={updateValues}
      {...item}
      {...initalFormProps.props.fields}
    />
  ));

  const submitForm = e => {
    e.preventDefault();

    initalFormProps.handlers.submit(formData);
  };

  const form = (
    <form onSubmit={submitForm}>
      {formFields}
      <Button type="submit" {...initalFormProps.props.submitButton}>
        Submit
      </Button>
    </form>
  );

  const api = {
    updateValues,
    updateValuesParams,
    setFormData,
    getFormFields: formFields
  };

  return [form, formData, api];
};

export default useForm;

```

It takes the `useFormData` hook from before and adds more components to it. Mainly it ads the `form` component and the `formFields` to the hook.

The hook now has 2 parameters when called.

##### - initialFormData
An object with the value that we want to initialise the `formData` with
##### - initalFormProps
An object with the configurations for the `form` 
- __fields__: Array with the fields objects
- __props__: Object with props for the fields components(_TextField_ in our case) and the submitButton component
- __handlers__: The handler for submit in this case

The hook is called as followed
```javascript
const Form = () => {
  const [form] = useForm(
    {},
    {
      fields: formFields,
      props: {
        fields: {
          fullWidth: true
        },
        submitButton: {
          fullWidth: true
        }
      },
      handlers: {
        submit: formData => console.log(formData)
      }
    }
  );

  return form;
};
```
[CodeSandbox example](https://codesandbox.io/s/abstraction-with-useform-hook-3cgtd)

The advantage of this custom hook is that you can override all of the methods whenever you need it.

If need only the fields from the from and not the plain form you can get them via the `api.getFormFileds` method and iterate them as you need.

I will write an article explaining and showing more example of this custom hook

## Optimisation <a name="chapter-3"></a>

My most common enemy was the re rendering of the components each time the `formData` object was changed. In small forms that is not an issue, but in big forms it will cause performance issues.

For that we will take advantage of the `useCallback` and `useMemo` hooks in order to optimise as much as we can in our hook.

The main idea was to memoize all the inputs and the form since it is initialised with a value, it should change only when the value is changed and not in any other case, so it will not trigger any unnecessary renders.

I ended up with the following code for the hook
```js
import React, { useState, useMemo, useCallback } from "react";
import { Button, TextField } from "@material-ui/core";

const useForm = (
  initialFormDataValue = {},
  initalFormProps = {
    fields: [],
    props: {
      fields: {},
      submitButton: {}
    },
    handlers: {
      submit: () => false
    }
  }
) => {
  const [formData, setFormData] = useState(initialFormDataValue);

  const updateValues = useCallback(
    ({ target: { name, value, type, checked } }) => {
      setFormData(prevData => ({
        ...prevData,
        [name]: type !== "chechbox" ? value : checked
      }));
    },
    []
  );

  const updateValuesParams = useCallback(
    (name, value) =>
      setFormData(prevData => ({
        ...prevData,
        [name]: value
      })),
    []
  );

  const formFields = useMemo(
    () =>
      initalFormProps.fields.map(item => (
        <TextField
          key={item.label}
          defaultValue={initialFormDataValue[item.name]}
          onChange={updateValues}
          {...item}
          {...initalFormProps.props.fields}
        />
      )),
    [updateValues, initalFormProps, initialFormDataValue]
  );

  const submitForm = useCallback(
    e => {
      e.preventDefault();

      initalFormProps.handlers.submit(formData);
    },
    [initalFormProps, formData]
  );

  const formProps = useMemo(
    () => ({
      onSubmit: submitForm
    }),
    [submitForm]
  );

  const submitButton = useMemo(
    () => (
      <Button type="submit" {...initalFormProps.props.submitButton}>
        Submit
      </Button>
    ),
    [initalFormProps]
  );

  const form = useMemo(
    () => (
      <form {...formProps}>
        {formFields}
        {submitButton}
      </form>
    ),
    [formFields, formProps, submitButton]
  );

  const api = useMemo(
    () => ({
      updateValues,
      updateValuesParams,
      setFormData,
      getFormFields: formFields
    }),
    [updateValues, updateValuesParams, setFormData, formFields]
  );
  return [form, formData, api];
};

export default useForm;
```

[CodeSandbox example](https://codesandbox.io/s/optimisation-with-useform-hook-do9f0)

##### Above and beyond

If we run the above example we would still have a render issue because of the `submitForm` callback, due to its `formData` dependency. 

It's not the perfect case scenario but it's a lot better than no optimisation at all

My solution for this was to move the `formData` in the store. Since my `submitHandler` is always `dispatch` and I only send the action, I was able to access the `formData` directly from Redux Saga and therefore remove the `formData` from the hook and also from the dependency array of `sumbitForm` callback. This may not work for others so I did not include this in the article.

If someone has any thoughts on how to solve the issue with the `formData` from the `submitForm` I would be glad to hear them