AiTechWorlds
AiTechWorlds
A custom hook is a plain JavaScript function, named starting with use, that extracts stateful logic out of a component into a standalone, reusable unit.
It works like a power strip versus wiring each appliance directly into the wall. Instead of rewriting the same useState + useEffect wiring in every component that needs it, you build the wiring once the custom hook and plug any component into it. Any time you see the same stateful pattern repeated in two components, a custom hook is the right abstraction.
A custom hook is a regular JavaScript function whose name starts with use and that calls other hooks inside it. That's it. No magic, no framework just a naming convention that React uses to enforce the Rules of Hooks.
// This is a custom hook
function useWindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight,
});
useEffect(() => {
function handleResize() {
setSize({ width: window.innerWidth, height: window.innerHeight });
}
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return size;
}
// Usage — same logic, no duplication
function Header() {
const { width } = useWindowSize();
return <nav className={width < 768 ? "mobile-nav" : "desktop-nav"}>...</nav>;
}
function Chart() {
const { width, height } = useWindowSize();
return <canvas width={width} height={height} />;
}Stop writing the same loading/error/data pattern in every component:
function useFetch<T>(url: string) {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
setError(null);
fetch(url)
.then(res => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
})
.then(data => {
if (!cancelled) {
setData(data);
setLoading(false);
}
})
.catch(err => {
if (!cancelled) {
setError(err);
setLoading(false);
}
});
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}
// Clean component — no fetch logic visible
function CourseList() {
const { data: courses, loading, error } = useFetch<Course[]>("/api/courses");
if (loading) return <Spinner />;
if (error) return <ErrorMessage message={error.message} />;
if (!courses) return null;
return <CourseGrid courses={courses} />;
}function useLocalStorage<T>(key: string, initialValue: T) {
const [stored, setStored] = useState<T>(() => {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
function setValue(value: T | ((prev: T) => T)) {
try {
const valueToStore = value instanceof Function ? value(stored) : value;
setStored(valueToStore);
localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (err) {
console.error(err);
}
}
return [stored, setValue] as const;
}
// Works exactly like useState, but persists to localStorage
function ThemeToggle() {
const [theme, setTheme] = useLocalStorage<"light" | "dark">("theme", "light");
return (
<button onClick={() => setTheme(t => t === "light" ? "dark" : "light")}>
Current: {theme}
</button>
);
}function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debounced;
}
function SearchPage() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 300);
// Only fetches after typing stops for 300ms
const { data: results } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);
return (
<div>
<input
value={query}
onChange={e => setQuery(e.target.value)}
placeholder="Search courses..."
/>
<ResultsList results={results} />
</div>
);
}function useIntersectionObserver(
ref: React.RefObject<Element>,
options?: IntersectionObserverInit
) {
const [isVisible, setIsVisible] = useState(false);
useEffect(() => {
if (!ref.current) return;
const observer = new IntersectionObserver(([entry]) => {
setIsVisible(entry.isIntersecting);
}, options);
observer.observe(ref.current);
return () => observer.disconnect();
}, [ref, options]);
return isVisible;
}
// Lazy-load images when they scroll into view
function LazyImage({ src, alt }: { src: string; alt: string }) {
const ref = useRef<HTMLDivElement>(null);
const isVisible = useIntersectionObserver(ref, { rootMargin: "100px" });
return (
<div ref={ref} className="w-full h-48 bg-gray-100 rounded-lg overflow-hidden">
{isVisible && (
<img src={src} alt={alt} className="w-full h-full object-cover" />
)}
</div>
);
}function useForm<T extends Record<string, string>>(initialValues: T) {
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Partial<Record<keyof T, string>>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
function handleChange(e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) {
const { name, value } = e.target;
setValues(prev => ({ ...prev, [name]: value }));
setErrors(prev => ({ ...prev, [name]: undefined }));
}
function handleBlur(e: React.FocusEvent<HTMLInputElement>) {
const { name } = e.target;
setTouched(prev => ({ ...prev, [name]: true }));
}
function setError(field: keyof T, message: string) {
setErrors(prev => ({ ...prev, [field]: message }));
}
function reset() {
setValues(initialValues);
setErrors({});
setTouched({});
}
return {
values,
errors,
touched,
handleChange,
handleBlur,
setError,
reset,
};
}
// Usage
function LoginForm() {
const { values, errors, touched, handleChange, handleBlur, setError } = useForm({
email: "",
password: "",
});
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
if (!values.email.includes("@")) {
setError("email", "Valid email required");
return;
}
await login(values.email, values.password);
}
return (
<form onSubmit={handleSubmit}>
<input
name="email"
value={values.email}
onChange={handleChange}
onBlur={handleBlur}
/>
{touched.email && errors.email && <p className="error">{errors.email}</p>}
{/* ... */}
</form>
);
}function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function PriceTracker({ price }: { price: number }) {
const prevPrice = usePrevious(price);
const direction = prevPrice !== undefined && price > prevPrice ? "up" : "down";
return <span className={direction === "up" ? "text-green-600" : "text-red-600"}>${price}</span>;
}When you find yourself adding the same useState + useEffect block in a second component, that's the moment to extract a hook. Ask:
useFetch, useDebounce, useAuth, useCartGood custom hooks have a clear responsibility, a descriptive name, and return exactly what's needed nothing more.
Next lesson: React.memo and avoiding re renders understanding React's rendering model and when to optimize.
Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises