What are React Hooks?
React’s built-in Hooks like useState and useEffect make it possible to manage state and lifecycle features directly in functional components. But when we need reusable logic across components, custom Hooks provide a way to keep our code organized and scalable.
Why are Hooks useful and when to use them
Hooks encapsulate reusable logic, making it easier to manage state and lifecycle features across components.
Custom Hooks are especially useful for handling forms, data fetching, and shared state, reducing redundancy and making our codebase scalable and maintainable.
What makes a function a ‘Hook’ in React?
A function is considered a Hook in React if it encapsulates reusable stateful logic and follows the naming convention by being prefixed with “use”.
The “use” prefix signals to React that it’s a Hook, allowing it to apply special internal rules (covered later) to manage the function’s behavior.
EXAMPLE
Source code available here on GitHub.
Our example app consists of forms that capture user information. It has two sections: ‘Personal Details’ and ‘Payment Details. Each section captures data and requires validation. Here’s how custom hooks simplify form handling and validation.
This is the structure of the app, we’ll be walking through these files specifically: PersonalDetailsForm.jsx
for defining form fields, useForm.js
custom Hook for managing state and submissions, and useValidation.js
custom Hook for centralized validation logic.
PersonalDetailsForm.jsx – Using useForm and validation schema
This is the component file for the ‘Personal Details’ form.
import { Input, Dropdown, Button } from "@/components";
import useForm from "@/hooks/useForm";
import { validations } from "@/util/validationHelper";
const PersonalDetailsForm = ({ title }) => {
const initialData = {
FirstName: "",
LastName: "",
Email: "",
ReceiveMarketing: false
};
// Validations
const validationSchema = {
FirstName: [
{
validator: validations.required,
message: "First name is required"
},
{
validator: (value) => validations.minLength(value, 2),
message: "First name must be at least two characters"
}
],
LastName: [
{
validator: validations.required,
message: "Last name is required"
}
],
Email: [
{
validator: validations.required,
message: "Email is required"
},
{ validator: validations.email, message: "Invalid email" }
]
};
// Custom Hook useForm for state management and validation handling
const { formData, handleInputChange, handleSubmit, validationProps } =
useForm(initialData, () => console.log(formData), validationSchema);
return (
<>
<div className="bg-white rounded-md p-6">
<form onSubmit={handleSubmit}>
<h2 className="text-2xl font-semibold text-orange-600 pb-4">
{title}
</h2>
<Input
label="First Name"
placeholder="John"
name="FirstName"
onChange={handleInputChange}
value={formData.FirstName}
{...validationProps("FirstName")}
/>
<Input
label="Last Name"
placeholder="Doe"
name="LastName"
onChange={handleInputChange}
value={formData.LastName}
{...validationProps("LastName")}
/>
<Input
label="Email"
placeholder="Email"
name="Email"
onChange={handleInputChange}
value={formData.Email}
{...validationProps("Email")}
/>
<Dropdown
label="Would you like to receive marketing emails from us?"
name="ReceiveMarketing"
onChange={handleInputChange}
value={formData.ReceiveMarketing}
items={[
{ label: "Yes", value: true },
{ label: "No", value: false }
]}
isRequired={false}
isValid={true}
/>
<Button type="submit" text="Submit" />
</form>
</div>
</>
);
};
export default PersonalDetailsForm;
In PersonalDetailsForm.jsx, we initialize initialData
to define field values and use validationSchema
to specify validation rules. These are passed to the custom Hook useForm
, which provides functions like handleInputChange
for updating form state and validationProps
for error handling and displaying error messages to the user.
useForm.js Custom Hook – Handling Form State and Submission
The custom useForm
Hook manages form state with formData
and handles submission. Form field changes are handled with handleInputChange
. This function parses different data types (like text, checkboxes and numbers) and validates each field on change, keeping our PersonaDetailsForm component clean.
import { useState } from "react";
import useValidation from "@/hooks/useValidation";
const useForm = (initialValues, onSubmitCallback, validationSchema = {}) => {
// Set form data state
const [formData, setFormData] = useState(initialValues);
// Calling another custom Hook - useValidation
const { validateFieldOnChange, validateAllFields, validationProps } =
useValidation(formData, validationSchema);
const handleInputChange = (e) => {
const { name, type, value, checked } = e.target;
const parsedValue =
type === "checkbox"
? checked
: value === "true" // Convert string representation of boolean to boolean
? true
: value === "false"
? false
: type === "number"
? value === ""
? ""
: parseFloat(value) // Parse as float if it's a number, allow empty string (for when user clears input)
: value; // e.target.value
setFormData({
...formData,
[name]: parsedValue
});
validateFieldOnChange(name, value);
};
const resetForm = () => {
const resetValues = Object.keys(formData).reduce((acc, key) => {
acc[key] = "";
return acc;
}, {});
setFormData(resetValues);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (validateAllFields()) {
try {
await onSubmitCallback(formData);
} catch (error) {
console.error("Error during form submission", error);
}
} else {
console.warn("Validation Failed");
alert("Invalid Form");
}
};
return {
formData,
handleInputChange,
resetForm,
handleSubmit,
validationProps
};
};
export default useForm;
useValidation.js Custom Hook - Centralised Validation Logic
The useValidation
custom Hook centralizes validation logic, with functions like validateFieldOnChange
to check fields in real-time and validateAllFields
to confirm all fields meet requirements before submission.
Using validationProps
, we can easily attach validation feedback to each field. Look at the input components in PersonalDetailsForm.jsx to see how the validation props are spread.
import { useState, useMemo } from "react";
const useValidation = (formData, validationSchema) => {
const [errors, setErrors] = useState({});
const memoValidationSchema = useMemo(
() => validationSchema,
[validationSchema]
);
// Validate field on change
const validateFieldOnChange = (name, value) => {
if (memoValidationSchema[name]) {
const fieldErrors = validateField(value, memoValidationSchema[name]);
setErrors((prevErrors) => ({
...prevErrors,
[name]: fieldErrors
}));
}
};
// Validate field against validation schema
const validateField = (value, rules) => {
let errors = [];
for (const rule of rules) {
if (
typeof rule === "object" &&
rule.validator &&
!rule.validator(value)
) {
errors.push(rule.message); // Add error message if validation fails
}
}
return errors;
};
// Validate all fields against their validation schema
const validateAllFields = () => {
const newErrors = [];
for (const field in memoValidationSchema) {
let fieldErrors = validateField(
formData[field],
memoValidationSchema[field]
);
if (fieldErrors.length > 0) {
newErrors[field] = fieldErrors;
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
};
// Validation props to use in form field component for error handling
const validationProps = useMemo(
() => (name) => ({
isValid: !errors?.[name] || errors?.[name].length === 0,
errorMessage: errors?.[name]?.[0]
}),
[errors]
);
return {
validateFieldOnChange,
validateAllFields,
validationProps
};
};
export default useValidation;
PaymentDetailsForm.jsx – Reusability
This is the component file for the ‘Payment Details’ form.
Our setup allows us to reuse useForm
and useValidation
in any form component such as PaymentDetailsForm.jsx without duplicating logic.
The alternative?
Without custom Hooks, each form would need its own state management and validation logic, leading to repetitive and cluttered code. Custom Hooks centralize this functionality, providing a reusable approach that follows the DRY principle.
Rules of Hooks
To ensure that Hooks work reliably, React enforces a few key rules to maintain consistent behaviour across components.
- Always call Hooks at the top level of your React function: Don’t use Hooks inside loops, conditions, nested functions, or after conditional returns. This ensures that Hooks are called in the same order on every render, which React relies on for tracking state correctly.
- Don’t call Hooks in event handlers or class components: Hooks are specifically designed for use in functional components or within custom Hooks, using them in other places may lead to unexpected behaviour.
- Use Hooks only within React functions or custom Hooks: Custom Hooks can call other Hooks, following the same rules, as shown in our example where the
useForm
Hook calls theuseValidation
Hook.
Reusability and maintainability are essential for building scalable applications. Custom Hooks in React allow us to achieve this by encapsulating logic that can be shared across components. This leads to cleaner, more modular code that’s easier to understand, test, and extend.
Top comments (2)
Simple but good hook for close modals when clicking outside of them.
gist.github.com/asmyshlyaev177/609...
This is version with Typescript and adjusted to work on mobile devices.
Good practice is to move to custom hooks repeating code.
Thanks for sharing this! That's a solid example for how custom hooks help with reusable code