Setting up forms in Front-End work could be a tedious work. There are multiple things to do that are repetitive like designing input, filling input properties, integrating with 3rd party library, and many more. My suggestion to tackle these problems is by making form components reusable.
Making form components reusable could greatly reduce time spent in developing form. Also our code will be much more readable since we are abstracting the way we write our form and its inputs. These could lead into better developer experience and also faster feature development. Here are step by step to make our form components.
Start by Translating Design System into Separate Components
The easiest way to decouple our components is by looking at the UI/UX design or mockups. From that we can get a glimpse of how we should structure our components. In the form context, I usually making each input type like text, number, date, etc. into separate components. Don't think about how our input integrated with entire form yet, start by designing our input component one by one. I personally think our input design should be isolated therefore whenever we need to improve our input, we know which CSS or other style API should we change.
This approach could make our code much shorter and readable. Take a look at this comparison.
First code
const MyForm = () => {
return (
<form>
<input
type="text"
id="full_name"
name="full_name"
value=""
placeholder='Full Name'
aria-placeholder='Full Name'
required
aria-required
/>
<input
type="email"
id="email"
name="email"
value=""
autoComplete='email'
placeholder='Email'
aria-placeholder='Email'
required
aria-required
/>
<input
type="password"
id="password"
name="password"
placeholder='Password'
aria-placeholder='Password'
value=""
required
aria-required
/>
</form>
)
}
Second code
const MyForm = () => {
return (
<form>
<TextInput name="full_name" placeholder="Full Name" required />
<EmailInput name="email" placeholder="Email" required />
<PasswordInput name="password" placeholder="Password" required />
</form>
)
}
Which one do you think more readable?
Integrate Inputs with Whole Form
There are multiple ways to integrate our inputs with the form. Natively, input should be controlled by useState
hooks by filling the value
and onChange
properties in input. For simple form, we can use this approach. It will look something like this.
const MyForm = () => {
const [name, setName] = useState<string>('')
const [email, setEmail] = useState<string>('')
const [password, setPassword] = useState<string>('')
return (
<form>
<TextInput
name="full_name"
placeholder="Full Name"
value={name}
onChange={(e) => setName(e.target.value)}
required
/>
<EmailInput
name="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
/>
<PasswordInput
name="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
/>
</form>
)
}
We need to define onChange
and value
properties in the input component. On the parent where the form exists, several states are defined. Each state corresponds to its input. They are passed down one by one for every input.
This approach is actually fine. But usually when I'm working with form, there's input validation on client side before the form is submitted. Also there are some cases where input is depended into the other input/s. Doing form natively can be pretty cumbersome since we need to manually declare everything from scratch.
I'd like to use library called react-hook-form
when doing form. By using this form we can eliminate repetitive code such as controlling form, validating, etc. I'm not gonna focus on why this library is great but how do we use this library to support our input components. In order to integrate this library with our input, we need to wrap our form with their context
called FormProvider
.
import { useForm, FormProvider } from 'react-hook-form'
interface MyFormValues {
name: string;
email: string;
password: string;
}
const MyForm = () => {
const methods = useForm<MyFormValues>({
defaultValues: {
name: '',
email: '',
password: '',
},
})
return (
<FormProvider {...methods}>
<form>
<TextInput name="full_name" placeholder="Full Name" required />
<EmailInput name="email" placeholder="Email" required />
<PasswordInput name="password" placeholder="Password" required />
</form>
</FormProvider>
)
}
react-hook-form
requires additional setup. FormProvider
wrapper needs to receive some properties that can be covered by using useForm
hook. This hook requires several arguments like the form values model for TypeScript (type or interface that represents all values in the form, this this case is MyFormValues) and configuration object as a function parameter. We can define the form default values inside the configuration object.
By using FormProvider
, any child components inside FormProvider
are able to access form metadata, including the current value and change handler. Next we need to modify our input components to integrate them with current FormProvider
.
import { useController } from 'react-hook-form'
interface TextInputProps {
name: string;
placeholder: string;
required?: boolean;
}
function TextInput(props: TextInputProps) {
const { field } = useController({
name: props.name,
defaultValue: ""
});
return (
<input
type="text"
id={props.name}
placeholder={props.placeholder}
aria-placeholder={props.placeholder}
required={props.required}
aria-required={props.required}
{...field}
/>
)
}
We can integrate our input with useController
hooks from react-hook-form
. This hook returns several variables, including field
variable that contains overridden input properties such as value
, name
, onChange
, onBlur
, etc. So we can leave out the input binding to the useController
hook.
From this step our form is already pretty much clear. The input components are already integrated with the form body so whenever we need to create new form, we just have to copy from this form with minimum code. But I'll give you another tips that could take the form development into another level.
Find Common Pattern in Our Forms
Once upon a time one of my works have to do with dashboard. Mostly the dashboard was filled with CRUD forms that deal with single data. The user interactions are also pretty much the same. I did like 3 forms by copy-pasting from one form to another and got bored. The flow was approximately like this,
Read / List
- Hit get all API to show data
- This API has several pagination parameter
Create
- Fill up form
- Hit create API
- Redirect user to data table with success message on successful create
- Show alert on failed create
Update
- Hit get detail API to fill up initial values on form
- Fill up form
- Hit update API
- Redirect user to data table with success message on successful update
- Show alert on failed update
Delete
- Hit delete button in the data list
- Confirmation dialog is opened
- Hit delete API if user prompts the deletion
- Refresh the list and show success message on successful delete
- Show alert on failed delete
There were approximately 20 forms with the same flow. Copy pasting over and over could lead into code duplication. Moreover if we need to change the form behavior, we have to revisit all of them one by one. So what did I do?
Create Custom Hook
Fortunately ReactJS has feature that could save our time copy pasting things, the custom hooks. That time I developed custom hooks that dealt with most of the flow so I could focus on form design and values model. Since there were 2 pages for each data CRUD, I create 2 custom hooks for each page. The hooks would be generic and receives some parameters like,
- Request Model
- Response Model
- API URL
- Mapper And returns variables that my form and list needs like data and handlers.
Of course there were drawbacks from doing this approach. Making things more general would make that things tightly coupled with each other. Whenever something change and doesn't fit with our current approach, we have to think the workaround which most of the time kinda ugly approach. The worst case is we have to rebuild everything again.
Also coordination with Back-End (if any) is important. Make sure the communication API is predictable and consistent over time. If something is out of place, our approach would be broken.
Closing
So that's all from me. What do you think of this article? Let me know your thoughts :D
If you like my article, make sure to click the ❤️ button.
Top comments (0)