What Are React Hooks?
Hooks are functions that let you "hook into" React state and lifecycle features from function components. Before hooks (introduced in React 16.8), you needed class components for state and lifecycle logic. Hooks made function components just as powerful — and much cleaner.
useState — Managing Local State
useState gives a component its own memory.
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>
);
}
Key rules:
useStatereturns a tuple:[currentValue, setter]- Calling the setter triggers a re-render
- Never mutate state directly — always use the setter
For objects, spread to avoid mutation:
const [user, setUser] = useState({ name: '', email: '' });
setUser(prev => ({ ...prev, name: 'Alex' }));
useEffect — Side Effects & Lifecycle
useEffect runs code after render. It replaces componentDidMount, componentDidUpdate, and componentWillUnmount.
import { useState, useEffect } from 'react';
function UserProfile({ userId }: { userId: string }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]); // re-runs when userId changes
return <div>{user?.name}</div>;
}
The dependency array:
[]— runs once on mount (like componentDidMount)[value]— runs on mount and whenevervaluechanges- Omitted — runs after every render (usually a bug)
Cleanup:
useEffect(() => {
const timer = setInterval(() => tick(), 1000);
return () => clearInterval(timer); // cleanup on unmount
}, []);
useRef — Accessing DOM & Persisting Values
useRef gives you a mutable container that doesn't trigger re-renders.
DOM access:
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
const focus = () => inputRef.current?.focus();
return (
<>
<input ref={inputRef} />
<button onClick={focus}>Focus</button>
</>
);
}
Persisting values across renders (without re-rendering):
const renderCount = useRef(0);
renderCount.current += 1;
useContext — Global State Without Prop Drilling
useContext lets any component read from a context without passing props down manually.
const ThemeContext = createContext<'light' | 'dark'>('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
function Toolbar() {
return <ThemedButton />;
}
function ThemedButton() {
const theme = useContext(ThemeContext);
return <button className={theme}>Click</button>;
}
Custom Hooks — Reusable Logic
Any function that starts with use and calls other hooks is a custom hook. They're the best way to share logic across components.
function useLocalStorage<T>(key: string, initialValue: T) {
const [value, setValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch {
return initialValue;
}
});
const setStoredValue = (newValue: T) => {
setValue(newValue);
localStorage.setItem(key, JSON.stringify(newValue));
};
return [value, setStoredValue] as const;
}
// Usage
const [theme, setTheme] = useLocalStorage('theme', 'light');
Rules of Hooks
- Only call hooks at the top level — not inside loops, conditions, or nested functions.
- Only call hooks from React functions — function components or custom hooks.
The ESLint plugin eslint-plugin-react-hooks enforces these automatically.
Conclusion
Hooks completely changed how React apps are written. Start with useState and useEffect — they cover 80% of use cases. Add useRef when you need DOM access or stable values, and useContext when prop drilling becomes painful. Once those feel natural, build custom hooks to encapsulate and share your logic cleanly.