What are Custom Hooks?
Custom hooks let you extract component logic into reusable functions. They're regular JavaScript functions that can use other hooks and follow the "use" naming convention.
Why Custom Hooks?
- Reuse stateful logic across components
- Keep components clean and focused
- Share logic without changing component hierarchy
- Make code more testable
📚 Hook Rules
- Start with "use": useMyHook, useAuth, useFetch
- Can call other hooks: useState, useEffect, etc.
- Top level only: Don't call in loops, conditions, or nested functions
- React components or hooks only: Can't use in regular functions
🎯 useLocalStorage
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get value from localStorage or use initialValue
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
// Save to 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 TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
// todos automatically sync with localStorage!
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]);
};
return <div>{/* render todos */}</div>;
}
🌐 useFetch
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 error! status: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
}
} 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 <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
🔄 useToggle
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(v => !v);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, toggle, setTrue, setFalse];
}
// Usage
function Modal() {
const [isOpen, toggle, open, close] = useToggle(false);
return (
<div>
<button onClick={open}>Open Modal</button>
{isOpen && (
<div className="modal">
<h2>Modal Content</h2>
<button onClick={close}>Close</button>
</div>
)}
</div>
);
}
⏱️ useDebounce
function useDebounce(value, delay = 500) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timeoutId = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timeoutId);
}, [value, delay]);
return debouncedValue;
}
// Usage
function SearchBox() {
const [search, setSearch] = useState('');
const debouncedSearch = useDebounce(search, 500);
const [results, setResults] = useState([]);
// Only fetch when debounced value changes
useEffect(() => {
if (debouncedSearch) {
fetch(`/api/search?q=${debouncedSearch}`)
.then(res => res.json())
.then(setResults);
} else {
setResults([]);
}
}, [debouncedSearch]);
return (
<div>
<input
value={search}
onChange={e => setSearch(e.target.value)}
placeholder="Search..."
/>
{results.map(r => <div key={r.id}>{r.name}</div>)}
</div>
);
}
📱 useMediaQuery
function useMediaQuery(query) {
const [matches, setMatches] = useState(() => {
return window.matchMedia(query).matches;
});
useEffect(() => {
const mediaQuery = window.matchMedia(query);
const handleChange = (e) => {
setMatches(e.matches);
};
mediaQuery.addEventListener('change', handleChange);
return () => {
mediaQuery.removeEventListener('change', handleChange);
};
}, [query]);
return matches;
}
// Usage
function ResponsiveComponent() {
const isMobile = useMediaQuery('(max-width: 768px)');
const isTablet = useMediaQuery('(min-width: 769px) and (max-width: 1024px)');
const isDesktop = useMediaQuery('(min-width: 1025px)');
return (
<div>
{isMobile && <MobileLayout />}
{isTablet && <TabletLayout />}
{isDesktop && <DesktopLayout />}
</div>
);
}
🔔 useOnClickOutside
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 && (
<div className="menu">
<a href="#">Item 1</a>
<a href="#">Item 2</a>
</div>
)}
</div>
);
}
⌨️ useKeyPress
function useKeyPress(targetKey) {
const [keyPressed, setKeyPressed] = useState(false);
useEffect(() => {
const downHandler = (e) => {
if (e.key === targetKey) {
setKeyPressed(true);
}
};
const upHandler = (e) => {
if (e.key === targetKey) {
setKeyPressed(false);
}
};
window.addEventListener('keydown', downHandler);
window.addEventListener('keyup', upHandler);
return () => {
window.removeEventListener('keydown', downHandler);
window.removeEventListener('keyup', upHandler);
};
}, [targetKey]);
return keyPressed;
}
// Usage
function Game() {
const leftPressed = useKeyPress('ArrowLeft');
const rightPressed = useKeyPress('ArrowRight');
const spacePressed = useKeyPress(' ');
return (
<div>
<p>Left arrow: {leftPressed ? '⬅️' : '➖'}</p>
<p>Right arrow: {rightPressed ? '➡️' : '➖'}</p>
<p>Space: {spacePressed ? '🚀' : '➖'}</p>
</div>
);
}
📜 useScrollPosition
function useScrollPosition() {
const [scrollPosition, setScrollPosition] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollPosition(window.pageYOffset);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
return scrollPosition;
}
// Usage
function ScrollIndicator() {
const scrollPosition = useScrollPosition();
const scrollPercentage = (scrollPosition / (document.documentElement.scrollHeight - window.innerHeight)) * 100;
return (
<div
className="scroll-indicator"
style={{ width: `${scrollPercentage}%` }}
/>
);
}
function BackToTop() {
const scrollPosition = useScrollPosition();
const showButton = scrollPosition > 300;
const scrollToTop = () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
};
return showButton ? (
<button onClick={scrollToTop} className="back-to-top">
↑ Top
</button>
) : null;
}
🎯 usePrevious
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
}, [value]);
return ref.current;
}
// Usage
function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<div>
<p>Current: {count}</p>
<p>Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
🔔 useInterval
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember latest callback
useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up interval
useEffect(() => {
if (delay === null) return;
const id = setInterval(() => {
savedCallback.current();
}, delay);
return () => clearInterval(id);
}, [delay]);
}
// Usage
function Clock() {
const [time, setTime] = useState(new Date());
useInterval(() => {
setTime(new Date());
}, 1000);
return (
<div>
<h2>{time.toLocaleTimeString()}</h2>
</div>
);
}
function Countdown({ seconds }) {
const [timeLeft, setTimeLeft] = useState(seconds);
useInterval(() => {
setTimeLeft(t => t - 1);
}, timeLeft > 0 ? 1000 : null); // Stop when 0
return <div>{timeLeft}s remaining</div>;
}
🎯 Key Takeaways
- Custom hooks: Reusable stateful logic
- Naming: Must start with "use"
- Can use hooks: useState, useEffect, etc.
- Share logic: Without changing component structure
- Testable: Test hooks independently
- Composable: Combine multiple hooks
- Follow rules: Top level only, no conditions
- Return values: Array or object, whatever makes sense