Scalable forms - React-TS - in 2 custom hooks and 1 object
Emmanuel Galindo
Posted on May 10, 2021
TL;DR
We are going to have an array of objects that defines our form. The properties from the objects are going to be the properties from the inputs from our form. These objects should have the same interface. The array could be store in the back-end or in a directory in the front-end, its up to you.
Then we are going to have a hook which return an object that will map the properties from the object properties one by one and returns the input component with the properties that we passed. For handle the form I will use the custom hook I created, use-form.
And with a map method we combine and created our form.
I don’t have an exclusive repository for this explanation but I applied it to one side proyect I have.
The render form hook is here: https://github.com/georgexx009/photos-app/blob/main/hooks/use-render-form.tsx
The form state hook is here: https://github.com/georgexx009/photos-app/blob/main/hooks/use-form.ts
The properties object is here: https://github.com/georgexx009/photos-app/blob/main/constants/photoForm.ts
Where it is consumed is here: https://github.com/georgexx009/photos-app/blob/main/components/PhotoForm/index.tsx
Situation that caused the creation of this solution
Writing forms is something that almost every coder do for a web application. Most of the times if your application is growing, you would need to add more inputs or delete inputs. Also you could have different forms around the app.
Having the above in mind means that when we write code, we should keep in mind scalability. This will help us to not have to pay technical debts in the future when we need to make changes to the code.
So in this post I’m going to explain one approach to make forms dynamic and scalable with ease.
Render form hook
This hook is basically to return an object, where each property key is a type of input. Example: text, select. You could define whatever works for you because all will be typed. In my app I created only for input and select.
The value of each key is a function, which has as parameter the properties and attributes need it for our input. The important ones to accomplish this are name, value and handleChange params. The name attribute is used by our form hook, later I’m going to explain how use-form works.
The parameter clearBtn is for my project, to show a button to clear the input value easily.
https://github.com/georgexx009/photos-app/blob/main/hooks/use-render-form.tsx
import React from 'react';
import { PhotoForm } from '@types';
import { Select, Input } from '@components'
export const useRenderForm = () => {
return {
text: ({
name,
label,
placeholder,
value,
handleChange,
clearBtn
}: PhotoForm) => (
<div className="mb-6" key={name}>
<label
htmlFor={name}
className="block mb-2 text-sm text-gray-600"
>
{label}
</label>
<Input
type="text"
name={name}
placeholder={placeholder}
value={value}
handleChange={handleChange}
clearBtn={clearBtn}
/>
</div>
),
select: ({
options,
value,
name,
handleChange,
label
}: PhotoForm) => (
<div className="mb-6" key={name}>
<label className="block mb-2 text-sm text-gray-600" htmlFor={name}>
{label}
</label>
<Select
options={options.map(option => ({
value: option,
label: option
}))}
name={name}
value={value}
handleChange={handleChange}
/>
</div>
)
};
}
Property object
This object will have the values from the params that use render form hook will use. This object could be place and consume in the most combinient way for your application.
The interface that I use is PhotoForm. If you see thats the interface from the params I passed to the function inside use render form hook.
export interface PhotoForm {
name: string;
label: string;
placeholder?: string;
type: 'text' | 'select';
value?: string;
defaultValue?: string;
handleChange?: (event: React.ChangeEvent<HTMLInputElement> | React.ChangeEvent<HTMLSelectElement>) => void;
options?: string[];
clearBtn?: boolean;
}
In type, I have a string literal type, these strings are the properties that we should have as properties from the object return by render form hook. This interface is like a mix from properties needed it from both, input text and select. The ones that are unique from an option type should be optional (?), to not cause errors in other parameters.
Most of the inputs I have are a Select component, so I show options. I created a string literal type from the values of the select options object.
export const photoOrientationOptions = [
'horizontal',
'vertical',
'square'
] as const;
export type PhotoOrientation = typeof photoOrientationOptions[number];
And an example of the properties object is:
export const photoFormProperties: PhotoForm[] = [
{
name: 'name',
label: 'Photo name',
type: 'text',
placeholder: 'photo name',
clearBtn: true
},
{
name: 'photoOrientation',
label: 'Photo orientation',
type: 'select',
defaultValue: 'horizontal',
options: (photoOrientationOptions as unknown) as string[]
}
]
What I know that could change is the property of options in case that we use a select, I like to use type literals or enums for the options, so this property would be any, because either way literal types are not going to be the same and the same goes for enums. (I set this property to string[] when I choose literal types, but I would need to set the literal type options to unknown and then to string[] with the assertion “as”.
Form Hook
This hook is for handle the state of the form and provide the value state and handleChange for every input in our form.
It is very important that the name attribute from the inputs is the same as the variable state.
I have also handleChangeFile for tiles because I needed it for the photo files from my app, but you could omit this.
import { ChangeEvent, useState } from 'react'
interface UseFormProps<T> {
initialState: T
}
export const useForm = <T>({ initialState }: UseFormProps<T>) => {
const [formState, setFormState] = useState<T>(initialState)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setFormState(prevState => ({
...prevState,
[event.target.name]: event.target.value
}))
}
const handleChangeFile = (event: ChangeEvent<HTMLInputElement>) => {
if (!event.target.files?.length) {
return;
}
setFormState(prevState => ({
...prevState,
[event.target.name]: event.target.files
}))
}
return {
formState,
setFormState,
handleChange,
handleChangeFile
}
}
Consume the hooks
The component were I consume everything is here:
https://github.com/georgexx009/photos-app/blob/main/components/PhotoForm/index.tsx
import { photoFormProperties } from 'constants/photoForm'
const { formState, setFormState, handleChangeFile, handleChange} = useForm<PhotoFormState>({ initialState: formInitialState })
const renderForm = useRenderForm()
{photoFormProperties.map(property => renderForm[property.type]({
...property,
value: formState[property.name],
handleChange: handleChange
}))}
Posted on May 10, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.