Chapter 2: Custom Hooks
What is a Custom Hook?
A custom hook is a JavaScript function that:
- Starts with
use(mandatory convention) - Can call other hooks
- Encapsulates reusable logic
// Custom hook
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// Usage
function Counter() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<p>{count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
Why Create Custom Hooks?
- Reusability: same logic in multiple components
- Separation: isolate logic from display
- Tests: easier to test than components
- Readability: simpler components
Example: useLocalStorage
Hook that synchronizes state with localStorage:
function useLocalStorage(key, initialValue) {
// Initialize from localStorage
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Update localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
}, [key, value]);
return [value, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'fr');
return (
<div>
<select value={theme} onChange={e => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={e => setLanguage(e.target.value)}>
<option value="fr">French</option>
<option value="en">English</option>
</select>
</div>
);
}
Example: useFetch
Hook for API calls:
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const json = await response.json();
if (!cancelled) {
setData(json);
}
} catch (err) {
if (!cancelled) {
setError(err.message);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return <div>{user.name}</div>;
}
Example: useDebounce
Hook to debounce a value (useful for search):
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(timer);
};
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) {
onSearch(debouncedQuery);
}
}, [debouncedQuery, onSearch]);
return (
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
Example: useToggle
Simple hook for boolean values:
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = useCallback(() => {
setValue(v => !v);
}, []);
const setTrue = useCallback(() => setValue(true), []);
const setFalse = useCallback(() => setValue(false), []);
return { value, toggle, setTrue, setFalse };
}
// Usage
function Modal() {
const { value: isOpen, toggle, setFalse: close } = useToggle();
return (
<div>
<button onClick={toggle}>Open/Close</button>
{isOpen && (
<div className="modal">
<p>Modal content</p>
<button onClick={close}>Close</button>
</div>
)}
</div>
);
}
Example: useForm
Hook for managing forms:
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
const handleChange = useCallback((e) => {
const { name, value, type, checked } = e.target;
setValues(prev => ({
...prev,
[name]: type === 'checkbox' ? checked : value
}));
}, []);
const setValue = useCallback((name, value) => {
setValues(prev => ({ ...prev, [name]: value }));
}, []);
const reset = useCallback(() => {
setValues(initialValues);
setErrors({});
}, [initialValues]);
const validate = useCallback((validationRules) => {
const newErrors = {};
for (const [field, rules] of Object.entries(validationRules)) {
for (const rule of rules) {
const error = rule(values[field], values);
if (error) {
newErrors[field] = error;
break;
}
}
}
setErrors(newErrors);
return Object.keys(newErrors).length === 0;
}, [values]);
return {
values,
errors,
handleChange,
setValue,
reset,
validate
};
}
// Usage
function LoginForm({ onSubmit }) {
const { values, errors, handleChange, validate } = useForm({
email: '',
password: ''
});
const validationRules = {
email: [
v => !v && 'Email required',
v => !v.includes('@') && 'Invalid email'
],
password: [
v => !v && 'Password required',
v => v.length < 6 && 'Minimum 6 characters'
]
};
const handleSubmit = (e) => {
e.preventDefault();
if (validate(validationRules)) {
onSubmit(values);
}
};
return (
<form onSubmit={handleSubmit}>
<div>
<input
name="email"
value={values.email}
onChange={handleChange}
placeholder="Email"
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div>
<input
name="password"
type="password"
value={values.password}
onChange={handleChange}
placeholder="Password"
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<button type="submit">Login</button>
</form>
);
}
Example: useOnClickOutside
Hook to detect clicks outside an element:
function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
// Usage
function Dropdown() {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef(null);
useOnClickOutside(dropdownRef, () => setIsOpen(false));
return (
<div ref={dropdownRef}>
<button onClick={() => setIsOpen(!isOpen)}>Menu</button>
{isOpen && (
<ul className="dropdown-menu">
<li>Option 1</li>
<li>Option 2</li>
<li>Option 3</li>
</ul>
)}
</div>
);
}
Custom Hooks Rules
- Name starts with
use: required for React to apply hook rules - Can call other hooks: useState, useEffect, useCallback, other custom hooks
- Returns what the component needs: values, functions, objects
- Each call is independent: two components using the same hook have separate states
File Organization
src/
hooks/
useLocalStorage.js
useFetch.js
useDebounce.js
useToggle.js
useForm.js
index.js // export all hooks
// hooks/index.js
export { useLocalStorage } from './useLocalStorage';
export { useFetch } from './useFetch';
export { useDebounce } from './useDebounce';
export { useToggle } from './useToggle';
export { useForm } from './useForm';
Exercises
Exercise 1: useWindowSize
Create a hook that returns window dimensions and updates on resize.
Solution:
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
return size;
}
// Usage
function ResponsiveComponent() {
const { width, height } = useWindowSize();
return (
<div>
Window: {width} x {height}
{width < 768 && <p>Mobile mode</p>}
</div>
);
}
Exercise 2: usePrevious
Create a hook that returns the previous value of a variable.
Solution:
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const previousCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCount}</p>
<button onClick={() => setCount(c => c + 1)}>+1</button>
</div>
);
}
Key Points to Remember
- Custom hook = function starting with
use - Encapsulates reusable logic between components
- Each call has its own state independent
- Can call other hooks (useState, useEffect, etc.)
- Simplifies components by extracting logic
<- Previous Chapter | Back to module | Next Chapter: Redux ->