Understanding React Hooks: A Visual Guide for Beginners
React Hooks explained through analogies, flip cards, and side-by-side comparisons. No prior React experience needed.
- Hooks let function components access React features previously only available in class components
- `useState` gives your component memory between renders
- `useEffect` runs code after rendering — for data fetching, timers, and subscriptions
- Always call hooks at the top level, never inside conditions or loops
- Always clean up effects that set up listeners or timers
React Hooks were introduced in 2019 and fundamentally changed how React developers write components. Before hooks, you had to use class-based components whenever you needed features like state or lifecycle methods. Now you can write simpler functions that do everything. If you have tried to learn hooks and found the documentation confusing, this guide takes a different approach.
What Is a Hook, Actually?
A hook is a special function that starts with the word use. When you call a hook inside a React component, it "hooks into" React's internal systems — letting your simple function access features that were previously only available to class components.
Think of it like a plug-in. Your component is a lamp. Hooks are the outlet. Without the outlet, the lamp does nothing. With the right hook, your component can remember things, talk to external APIs, and respond to changes in the page.
The Two Hooks You Will Use Most
useState: Making Components Remember
Without useState, your component forgets everything every time React re-renders it. Variables reset. Counters go back to zero. useState is the solution.
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}The syntax looks unusual at first. useState(0) returns an array with two things: the current value and a function to update it. The [count, setCount] part unpacks that array. You name them whatever makes sense.
useEffect: Running Code at the Right Moment
useEffect runs after your component appears on screen. It is where you fetch data, start timers, or do anything that needs to happen after rendering.
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
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>Hello, {user.name}</p>;
}The array at the end — [userId] — is called the dependency array. It tells React when to re-run the effect. If userId changes, the effect runs again and fetches new data.
Before and After Hooks
The best way to appreciate hooks is to see what code looked like before them.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<button onClick={() =>
this.setState({
count: this.state.count + 1
})
}>
{this.state.count}
</button>
);
}
}More boilerplate. Requires understanding this. Constructor needed for any state. Harder to extract logic.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() =>
setCount(count + 1)
}>
{count}
</button>
);
}Shorter. No this confusion. No constructor. Logic is easy to extract into custom hooks. Reads top to bottom.
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<button onClick={() =>
this.setState({
count: this.state.count + 1
})
}>
{this.state.count}
</button>
);
}
}More boilerplate. Requires understanding this. Constructor needed for any state. Harder to extract logic.
function Counter() {
const [count, setCount] = useState(0);
return (
<button onClick={() =>
setCount(count + 1)
}>
{count}
</button>
);
}Shorter. No this confusion. No constructor. Logic is easy to extract into custom hooks. Reads top to bottom.
The functional version with hooks is not just shorter — it is genuinely easier to understand and test.
The Rules of Hooks
Why these rules? React keeps a list of hook values in the order you call them. If you call useState inside an if block, React might skip it on some renders, shifting every hook that comes after it and breaking the whole component.
Custom Hooks: Sharing Logic Between Components
One of the most powerful features of hooks is that you can create your own. A custom hook is just a function that starts with use and calls other hooks inside it.
// Custom hook: useWindowWidth
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;
}
// Using it in any component:
function ResponsiveComponent() {
const width = useWindowWidth();
return <p>Window is {width}px wide</p>;
}Notice the return statement inside useEffect — that is the cleanup function. It runs when the component is removed from the page, preventing memory leaks. Whenever you set up a listener or timer in useEffect, always clean it up in a return function.
useWindowWidth tells you the result. useResizeEventListener describes the implementation. The first name is more useful when reading components.Key Takeaways
- Hooks let function components access React features previously only available in class components
useStategives your component memory between rendersuseEffectruns code after rendering — for data fetching, timers, and subscriptions- Always call hooks at the top level, never inside conditions or loops
- Always clean up effects that set up listeners or timers
- Custom hooks (starting with
use) let you share logic between components without duplicating code
Finished reading? Mark this article to track your progress.