Setting up the reducer
Our useForm
hook only takes in one parameter: the initialState
of the form. For this hook, I’m going to use a reducer to keep track of and update the form data. Let’s create an "UPDATE"
action type that takes in a field name and value, and updates our state accordingly.
useForm.js
import { useReducer } from "react";
export function useForm(initialState) {
function reducer(state, action) {
switch (action.type) {
case "UPDATE":
return {
...state,
[action.field]: action.value,
};
default:
return state;
}
}
const [formData, dispatch] = useReducer(reducer, initialState);
}
Simplifying the code
From there, we could export formData
and dispatch
from our useForm
hook, and use them in our Form component like so:
Form.jsx
<input
name="fullName"
value={formData.fullName}
onChange={(e) =>
dispatch({
type: "UPDATE",
name: "fullName",
value: e.target.value,
})
}
/>
But if we have a lot of fields, this might end up becoming a bit repetitive. So let’s go back to our useHook
file and add a new updateFormField
function that we can use instead of dispatch
.
useForm.js
const updateFormField = (e) => {
dispatch({
type: "UPDATE",
inputType: e.target.type,
value: e.target.value,
field: e.target.name,
});
};
return { formData, updateFormField };
Instead of just receiving the field’s name and value, this function takes in the onChange
event as a parameter. This allows us to get the info we need from the event, and simplify our input to this:
Form.js
<input
name="fullName"
value={formData.fullName}
onChange={(e) => updateFormField(e)}
/>
Handling number inputs
There’s one more improvement we can make to our code: in our updateFormField
function, you’ll notice that we dispatch an inputType
variable that gets its value from the input’s type
attribute.
This allows us to parse the value of the input’s change event to a number for number
inputs:
Form.js
function reducer(state, action) {
switch (action.type) {
case "UPDATE":
return {
...state,
[action.field]:
action.inputType === "number" ? parseInt(action.value) : action.value,
};
default:
return state;
}
}
Of course, this might not cover 100% of use cases; for example, you might need a fractional number rather than an integer. But this hook is a good starting point.
The hook
This is what our base hook will look like.
type Action = {
type: "UPDATE";
field: string;
inputType?: string;
value: string;
};
export function useForm<FormData>(initialState: FormData): {
formData: FormData;
updateFormField: (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => void;
} {
function reducer(state: FormData, action: Action) {
switch (action.type) {
case "UPDATE":
return {
...state,
[action.field]:
action.inputType === "number"
? parseInt(action.value)
: action.value,
};
default:
return state;
}
}
const [formData, dispatch] = useReducer(reducer, initialState);
const updateFormField = (
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
dispatch({
type: "UPDATE",
inputType: e.target.type,
value: e.target.value,
field: e.target.name,
});
};
return { formData, updateFormField };
}
import { useReducer } from "react";
export function useForm(initialState) {
function reducer(state, action) {
switch (action.type) {
case "UPDATE":
return {
...state,
[action.field]:
action.inputType === "number"
? parseInt(action.value)
: action.value,
};
default:
return state;
}
}
const [formData, dispatch] = useReducer(reducer, initialState);
const updateFormField = (e) => {
dispatch({
type: "UPDATE",
inputType: e.target.type,
value: e.target.value,
field: e.target.name,
});
};
return { formData, updateFormField };
}
Usage
To make use of the useForm
hook in a form component, we just need to import it and set its initial state:
import { useForm } from "hooks/useForm";
type FormDataType = {
fullName: string;
birthYear: number;
};
export default function Form() {
const { formData, updateFormField } = useForm<FormDataType>({
fullName: "John Doe",
birthYear: 1990,
});
return (
<form>
<label htmlFor="fullName">Full name</label>
<input
type="text"
name="fullName"
value={formData.fullName}
onChange={(e) => updateFormField(e)}
required
/>
<label htmlFor="birthYear">Birth year</label>
<input
type="number"
name="birthYear"
value={formData.birthYear}
onChange={(e) => updateFormField(e)}
required
/>
<input type="submit" value="Submit" />
</form>
);
}
import { useForm } from "hooks/useForm";
export default function Form() {
const { formData, updateFormField } = useForm({
fullName: "John Doe",
birthYear: 1990,
});
return (
<form role="search" action="/search">
<label htmlFor="fullName">Full name</label>
<input
type="text"
name="fullName"
value={formData.fullName}
onChange={(e) => updateFormField(e)}
required
/>
<label htmlFor="birthYear">Birth year</label>
<input
type="number"
name="birthYear"
value={formData.birthYear}
onChange={(e) => updateFormField(e)}
required
/>
<input type="submit" value="Submit" />
</form>
);
}
Demo
import "./styles.css"; import { useForm } from "./hooks/useForm"; type FormDataType = { fullName: string; birthYear: number; }; const initialData: FormDataType = { fullName: "John Doe", birthYear: 1990 }; export default function Form() { const { formData, updateFormField } = useForm<FormDataType>(initialData); return ( <form> <label htmlFor="fullName">Full name</label> <input type="text" name="fullName" autoComplete="name" value={formData.fullName} onChange={(e) => updateFormField(e)} required /> <label htmlFor="birthYear">Birth year</label> <input type="number" name="birthYear" autoComplete="bday-year" value={formData.birthYear} onChange={(e) => updateFormField(e)} required /> <input type="submit" value="Submit" /> <output name="result" htmlFor="fullName birthYear"> {JSON.stringify(formData)} </output> </form> ); }