
Before React 16.8, sharing stateful logic between components required complex patterns like higher-order components (HOCs) or render props. Hooks changed everything. They let you use state and other React features inside function components, keeping your code simpler and more reusable.
useState is the most fundamental hook. It returns a state value and a setter
function:
import { useState } from "react";
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
Every time setCount is called, React re-renders the component with the new
value.
useEffect is for code that interacts with the outside world — fetching data,
setting up subscriptions, or updating the document title:
import { useState, useEffect } from "react";
function UserProfile({ userId }: { userId: number }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then((res) => res.json())
.then((data) => setUser(data));
}, [userId]); // re-runs whenever userId changes
if (!user) return <p>Loading...</p>;
return <p>{user.name}</p>;
}
The dependency array ([userId]) controls when the effect re-runs. An empty
array [] means run once on mount.
useRef stores a value that persists between renders without triggering a
re-render. Most commonly used to reference DOM elements:
import { useRef } from "react";
function FocusInput() {
const inputRef = useRef<HTMLInputElement>(null);
return (
<>
<input ref={inputRef} type="text" />
<button onClick={() => inputRef.current?.focus()}>Focus</button>
</>
);
}
useContext lets any component read from a React Context without prop-drilling:
import { createContext, useContext } from "react";
const ThemeContext = createContext<"light" | "dark">("light");
function ThemedButton() {
const theme = useContext(ThemeContext);
return (
<button
className={theme === "dark" ? "bg-gray-800 text-white" : "bg-white"}
>
Click me
</button>
);
}
useMemo caches the result of a calculation so it doesn't recompute on every
render:
import { useMemo } from "react";
function SortedList({ items }: { items: number[] }) {
const sorted = useMemo(() => [...items].sort((a, b) => a - b), [items]);
return (
<ul>
{sorted.map((n) => (
<li key={n}>{n}</li>
))}
</ul>
);
}
Only reach for useMemo when you have a provably expensive computation —
premature optimisation can add complexity without benefit.
The real power of hooks is composability. You can extract reusable logic into your own custom hooks:
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
return width;
}
Any component can now call useWindowWidth() to get the current viewport width
reactively.
Two rules to always follow:
Hooks make React components cleaner, more testable, and far easier to share
logic across your codebase. Master useState and useEffect first, then
gradually adopt useRef, useContext, and useMemo as your needs grow.