Custom Hooks in React
Definition: Custom Hooks are JavaScript functions whose names start with "use" and that may call other Hooks inside them. They let you reuse stateful logic between components without changing the component hierarchy.
On this page
Custom Hooks in React
What are Custom Hooks?
Definition: Custom Hooks are JavaScript functions whose names start with "use" and that may call other Hooks inside them. They let you reuse stateful logic between components without changing the component hierarchy.
Use cases:
- Reusing complex logic across multiple components
- Separating business logic from UI logic
- Creating simpler APIs for complex tasks
- Fetching data with similar patterns
- Form validation and management
- Local storage synchronization
- API integration patterns
Rules for creating Custom Hooks
1. Naming rule
- Required: The name must start with "use" (e.g.,
useAuth,useFetch) - Reason: So React can apply the rules of Hooks
2. Rules for calling Hooks
- Only call at the top level: Don't call them inside loops, conditions, or nested functions
- Consistent order: Make sure the order Hooks are called in stays the same across renders
3. Design principles
- Reuse logic, not UI: Custom Hooks share stateful logic, not JSX
- Independent state: Each call to a Custom Hook creates independent state
- Single responsibility: Each hook should have one clear purpose
Benefits of Custom Hooks
| Benefit | Description |
|---|---|
| Code reusability | Reuse logic across multiple components |
| Separation of concerns | Separates business logic from UI logic |
| Easier testing | Test logic independently of the UI |
| Cleaner components | Components focus on rendering, logic lives in hooks |
| Better organization | Groups related state and logic together |
When should you create a Custom Hook?
✅ You should create one when:
- The logic is used in multiple components
- A component becomes too complex
- You need to separate business logic from the UI
- You want to test logic independently
❌ You shouldn't create one when:
- The logic is only used in one component
- The logic is too simple (1-2 lines of code)
- There isn't a clear pattern to abstract yet
1. Custom Hook for Fetching Data
Definition: A custom hook for handling API calls and managing loading, error, and data states.
Use cases:
- Calling an API to get data from the server
- Managing loading state and error handling
- Reusing data-fetching logic across components
- Caching and retry mechanisms
Example:
// hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (url) {
fetchData();
}
}, [url]);
return { data, loading, error };
}
// Used in a component
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<ul>
{users?.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
2. Custom Hook for Local Storage
Definition: A hook that syncs state with localStorage, automatically saving and restoring data.
Use cases:
- Storing user preferences (theme, language)
- Shopping cart persistence
- Form data backup
- User settings and configuration
Example:
// hooks/useLocalStorage.js
import { useState } from 'react';
function useLocalStorage(key, initialValue) {
// Get the value from localStorage or use initialValue
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});
// Function to set the value into state and localStorage
const setValue = (value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error writing localStorage key "${key}":`, error);
}
};
return [storedValue, setValue];
}
// Used in a component
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'vi');
return (
<div>
<h2>Settings</h2>
<div>
<label>Theme: </label>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
<div>
<label>Language: </label>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="vi">Vietnamese</option>
<option value="en">English</option>
</select>
</div>
</div>
);
}
3. Custom Hook for Form Management
Definition: A hook that manages form state, validation, and common form operations.
Use cases:
- Managing form data and validation
- Handling form submission
- Field-level validation
- Form reset and error handling
Example:
// hooks/useForm.js
import { useState } from 'react';
function useForm(initialValues, validationRules = {}) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const [touched, setTouched] = useState({});
const handleChange = (name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
if (errors[name]) {
setErrors(prev => ({ ...prev, [name]: '' }));
}
};
const handleBlur = (name) => {
setTouched(prev => ({ ...prev, [name]: true }));
validateField(name, values[name]);
};
const validateField = (name, value) => {
const rule = validationRules[name];
if (!rule) return;
let error = '';
if (rule.required && (!value || value.trim() === '')) {
error = rule.required;
} else if (rule.minLength && value.length < rule.minLength) {
error = rule.minLength;
} else if (rule.pattern && !rule.pattern.test(value)) {
error = rule.patternMessage || 'Invalid format';
}
setErrors(prev => ({ ...prev, [name]: error }));
return error === '';
};
const validateForm = () => {
let isValid = true;
Object.keys(validationRules).forEach(fieldName => {
const isFieldValid = validateField(fieldName, values[fieldName]);
if (!isFieldValid) isValid = false;
});
return isValid;
};
const reset = () => {
setValues(initialValues);
setErrors({});
setTouched({});
};
return {
values,
errors,
touched,
handleChange,
handleBlur,
validateForm,
reset,
isValid: Object.keys(errors).length === 0
};
}
// Used in a component
function LoginForm() {
const validationRules = {
email: {
required: 'Email is required',
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
patternMessage: 'Invalid email'
},
password: {
required: 'Password is required',
minLength: 'Password must be at least 6 characters'
}
};
const {
values,
errors,
touched,
handleChange,
handleBlur,
validateForm
} = useForm({ email: '', password: '' }, validationRules);
const handleSubmit = (e) => {
e.preventDefault();
if (validateForm()) {
console.log('Login successful:', values);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
type="email"
placeholder="Email"
value={values.email}
onChange={(e) => handleChange('email', e.target.value)}
onBlur={() => handleBlur('email')}
/>
{touched.email && errors.email && (
<span style={{ color: 'red' }}>{errors.email}</span>
)}
</div>
<div>
<input
type="password"
placeholder="Password"
value={values.password}
onChange={(e) => handleChange('password', e.target.value)}
onBlur={() => handleBlur('password')}
/>
{touched.password && errors.password && (
<span style={{ color: 'red' }}>{errors.password}</span>
)}
</div>
<button type="submit">Log in</button>
</form>
);
}
4. Custom Hook for Toggle State
Definition: A simple hook for managing boolean state with toggle and set true/false operations.
Use cases:
- Opening/closing a modal, dropdown, sidebar
- Show/hide content
- Enable/disable features
- Toggle switches and checkboxes
Example:
// hooks/useToggle.js
import { useState, useCallback } from 'react';
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => setValue(prev => !prev), []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Used in a component
function Modal() {
const { value: isOpen, setTrue: openModal, setFalse: closeModal } = useToggle();
return (
<div>
<button onClick={openModal}>Open Modal</button>
{isOpen && (
<div className="modal-overlay" onClick={closeModal}>
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
<h2>This is a Modal</h2>
<p>Modal content goes here...</p>
<button onClick={closeModal}>Close</button>
</div>
</div>
)}
</div>
);
}
5. Custom Hook for Pagination
Definition: A hook that manages pagination, including fetching data per page and navigating between pages.
Use cases:
- Paginating a product list
- Pagination for a data table
- Infinite scroll implementation
- Load-more functionality
Example:
// hooks/usePagination.js
import { useState, useEffect, useCallback } from 'react';
function usePagination(fetchFunction, initialPage = 1, pageSize = 10) {
const [data, setData] = useState([]);
const [currentPage, setCurrentPage] = useState(initialPage);
const [totalPages, setTotalPages] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const fetchData = useCallback(async (page) => {
try {
setLoading(true);
setError(null);
const response = await fetchFunction({ page, limit: pageSize });
setData(response.data);
setTotalPages(Math.ceil(response.total / pageSize));
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}, [fetchFunction, pageSize]);
useEffect(() => {
fetchData(currentPage);
}, [currentPage, fetchData]);
const goToPage = (page) => {
if (page >= 1 && page <= totalPages) {
setCurrentPage(page);
}
};
const nextPage = () => currentPage < totalPages && setCurrentPage(prev => prev + 1);
const prevPage = () => currentPage > 1 && setCurrentPage(prev => prev - 1);
return {
data,
currentPage,
totalPages,
loading,
error,
goToPage,
nextPage,
prevPage,
hasNext: currentPage < totalPages,
hasPrev: currentPage > 1
};
}
// Used in a component
function ProductList() {
const fetchProducts = async ({ page, limit }) => {
const response = await fetch(`/api/products?page=${page}&limit=${limit}`);
return response.json();
};
const {
data: products,
currentPage,
totalPages,
loading,
nextPage,
prevPage,
hasNext,
hasPrev
} = usePagination(fetchProducts, 1, 5);
if (loading) return <div>Loading...</div>;
return (
<div>
<h2>Product List</h2>
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
<div className="pagination">
<button onClick={prevPage} disabled={!hasPrev}>
← Previous page
</button>
<span>Page {currentPage} / {totalPages}</span>
<button onClick={nextPage} disabled={!hasNext}>
Next page →
</button>
</div>
</div>
);
}
Conclusion: Custom Hooks are a powerful tool that helps keep React code clean, reusable, and easy to maintain. Use them to optimize the structure of your project!
