React hooks are special functions that let you add features to functional components. They were introduced in React 16.8 to replace the need for class components.
The hooks covered in this document are useState, useEffect, and useContext. The hooks useReducer, useMemo, useCallback, and useRef each have their own dedicated topics. React also supports custom hooks — covered at the end of this document.
useState
useState lets you add state (memory) to a functional component. When state changes, React re-renders the component with the new value. This hook is covered in full depth in its own dedicated topic. Here is a quick reference:
const [count, setCount] = useState(0);
See the useState topic for the full breakdown including the updater function pattern, object and array state, async behaviour, and TypeScript typing.
useEffect
useEffect is React's way of handling side effects in functional components. A side effect is anything that happens outside of rendering UI — API calls, timers, subscriptions, logging, or direct DOM changes.
Think of it as: "Now that this UI is showing (or has updated), do this extra thing."
useEffect has three parts:
- Effect function — the main body that runs after the component renders.
- Cleanup function — an optional function returned from the effect that runs when the component unmounts or before the effect re-runs.
- Dependency array — tells React when to run the effect.
useEffect(() => {
// Effect function — runs after render
return () => {
// Cleanup function — runs on unmount or before re-run
};
}, []); // Dependency array
The Dependency Array
| Dependency Array | When the Effect Runs |
|---|---|
[] | Once — when the component first mounts |
[value] | On mount and whenever value changes |
| No array | After every render (rarely what you want) |
The Cleanup Function
The cleanup function prevents memory leaks by stopping processes that should no longer run when a component is removed. A timer is a classic example — without cleanup, the timer keeps running in the background even after the component is gone.
useEffect(() => {
const timer = setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(timer); // Stops the timer when the component unmounts
};
}, []);
setInterval, clearInterval, and setTimeout
These are JavaScript built-ins commonly used inside useEffect:
| Function | What It Does |
|---|---|
setInterval(fn, ms) | Runs fn repeatedly every ms milliseconds. Returns an ID. |
clearInterval(id) | Stops the interval with the given ID. |
setTimeout(fn, ms) | Runs fn once after ms milliseconds. |
The key reason these belong inside useEffect is that placing them directly in the component body would cause them to reset or duplicate on every render.
Real-World Example: Timer
import { useState, useEffect } from "react";
function TimerExample() {
const [count, setCount] = useState(0);
const [time, setTime] = useState(0);
useEffect(() => {
console.log("Component mounted");
const timer = setInterval(() => {
setTime((prev) => prev + 1);
}, 1000);
return () => {
clearInterval(timer);
console.log("Component unmounted, timer cleared");
};
}, []); // Runs once on mount
return (
<div style={{ textAlign: "center", marginTop: "50px" }}>
<h1>⏳ Timer: {time} seconds</h1>
<h2>Clicked {count} times</h2>
<button onClick={() => setCount(count + 1)}>Click Me</button>
</div>
);
}
export default TimerExample;
useContext
useContext lets you consume context values directly inside a functional component. Context is React's way of sharing data globally — like theme, user info, language, or auth state — without prop drilling.
Prop drilling means manually passing props down through every intermediate component in the tree, even if only a deeply nested child actually needs the data. Context solves this by creating a shared "bucket" that any component in the tree can read from directly.
How It Works
There are three steps:
1. Create the context
const MyContext = createContext(defaultValue);
2. Provide the context value high in the component tree
<MyContext.Provider value={someValue}>
<App />
</MyContext.Provider>
3. Consume the context value with useContext
const value = useContext(MyContext);
Features of useContext
- Direct access — no need for the older
<MyContext.Consumer>wrapper pattern. - Auto updates — all components consuming a context re-render automatically when its value changes.
- Lightweight — no external libraries needed for simpler shared state scenarios.
- Scoped — each
<Provider>creates its own isolated context.
When to Use useContext
- The same data is needed by many components (theme, user, auth, settings).
- You want to avoid prop drilling.
- You want a simpler alternative to Redux for smaller apps.
Example: Theme Toggle
import { useState, createContext, useContext } from "react";
// 1. Create the context
const ThemeContext = createContext("light");
// 2. Parent component — provides the context value
function App() {
const [theme, setTheme] = useState("light");
const toggleTheme = () => {
setTheme((prev) => (prev === "light" ? "dark" : "light"));
};
return (
<ThemeContext.Provider value={theme}>
<div
style={{
textAlign: "center",
marginTop: "50px",
padding: "20px",
backgroundColor: theme === "light" ? "#fff" : "#333",
color: theme === "light" ? "#000" : "#fff",
}}
>
<h1>Current Theme: {theme}</h1>
<button onClick={toggleTheme}>Toggle Theme</button>
<ThemedComponent />
</div>
</ThemeContext.Provider>
);
}
// 3. Child component — consumes the context directly
function ThemedComponent() {
const theme = useContext(ThemeContext);
return <h2>The theme inside ThemedComponent is: {theme}</h2>;
}
export default App;
Breakdown
createContext()— creates the context and defines the default value.<ThemeContext.Provider value={theme}>— makes thethemevalue available to all descendant components. Think of it as opening the bucket and letting everyone below see its contents.ThemedComponent— callsuseContext(ThemeContext)to read the value directly, without receiving it as a prop. No intermediate component needs to pass it down.
Custom Hooks
A custom hook is a JavaScript function whose name starts with use and that calls one or more built-in React hooks inside it. Custom hooks let you extract and reuse stateful logic across multiple components — keeping your components clean and your logic in one place.
For example, instead of writing the same useState + useEffect logic for fetching data in every component that needs it, you can extract it into a useFetch hook:
import { useState, useEffect } from "react";
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json) => {
setData(json);
setLoading(false);
})
.catch((err) => {
setError(err);
setLoading(false);
});
}, [url]);
return { data, loading, error };
}
Using it in any component:
function UserProfile() {
const { data, loading, error } = useFetch("https://api.example.com/user/1");
if (loading) return <p>Loading...</p>;
if (error) return <p>Error loading data.</p>;
return <h2>{data.name}</h2>;
}
The naming convention use prefix is not just a style rule — React uses it to identify hooks and enforce the rules of hooks (they must be called at the top level, not inside loops or conditions).