If you’re learning React, you’ve probably heard about React Hooks and for good reason. Hooks completely changed how we write React components by making it easier to reuse logic and manage state without dealing with complex class components. Whether you’re new to React or coming from class components, this guide will walk you through everything you need to know to build your first hook-based app.
What Are React Hooks? (And Why Should You Care?)
Before React 16.8, state management was only possible in class components. If you wanted to use state in a functional component, you were out of luck. Then came Hooks a revolutionary feature that lets you use state and other React features directly in functional components.
Think of Hooks as special functions that “hook into” React features. They’re optional, backward-compatible, and they make your code cleaner and easier to maintain. The best part? They’re actually fun to use once you understand them.
The Two Essential Hooks You Need to Know
1. useState: Your Component’s Memory
The useState hook is the bread and butter of React Hooks. It allows you to add state to your functional component, giving it “memory” that persists between renders.
Here’s how it works:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Let me break this down:
-
const [count, setCount] = useState(0);— This is destructuring.countholds your current state value, andsetCountis the function you use to update it. The0is your initial state. -
count— This is your state variable. It starts at 0. -
setCount— This is the function that updatescount. When you callsetCount(5), React knows to re-render your component with the new value.
Why this matters: When the user clicks the button, setCount updates the state, React re-renders the component, and the count increases on screen. That’s the magic of useState.
Using Multiple State Variables:
You can call useState multiple times in a single component. Each one gets its own independent state:
function UserForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState('');
return (
<div>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
<input
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter email"
/>
<input
value={age}
onChange={(e) => setAge(e.target.value)}
placeholder="Enter age"
/>
</div>
);
}
2. useEffect: Side Effects Made Simple
While useState manages state, useEffect handles side effects—things like fetching data from an API, updating the page title, or subscribing to events. Side effects are actions that happen outside your component’s normal rendering flow.
import React, { useState, useEffect } from 'react';
function Greeting() {
const [name, setName] = useState('World');
useEffect(() => {
document.title = `Hello, ${name}!`;
}, [name]);
return (
<div>
<h1>Hello, {name}!</h1>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
Here’s what’s happening:
-
The effect function — This is the code inside
useEffect(() => {...}). It runs after React renders your component. -
The dependency array — That
[name]at the end tells React when to run the effect. In this case, it runs after every render wherenamechanges. If you leave it empty[], the effect runs only once after the first render. If you remove the array entirely, it runs after every render.
A Real-World Example: Fetching Data
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <p>Loading...</p>;
if (!user) return <p>No user found</p>;
return <div>{user.name}</div>;
}
This component fetches a user from an API when the component mounts or when userId changes. The loading state keeps track of whether the data is still being fetched.
Building Your First Hook-Based App: A Todo List
Now let’s bring it all together and build a simple todo app—the perfect project to understand Hooks in action.
Step 1: Set Up Your React Project
First, create a new React app:
npx create-react-app my-todo-app
cd my-todo-app
npm start
Step 2: Create the Todo Component
Replace the contents of src/App.js with this:
import React, { useState } from 'react';
import './App.css';
function App() {
const [todos, setTodos] = useState([
{ id: 1, text: 'Learn React Hooks', completed: false },
{ id: 2, text: 'Build a Todo App', completed: false },
{ id: 3, text: 'Master useEffect', completed: false }
]);
const [input, setInput] = useState('');
// Add a new todo
const addTodo = () => {
if (input.trim() === '') return;
const newTodo = {
id: Date.now(),
text: input,
completed: false
};
setTodos([...todos, newTodo]);
setInput('');
};
// Toggle todo completion
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
// Delete a todo
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div className="app">
<h1>My Todo List</h1>
<div className="input-container">
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && addTodo()}
placeholder="Add a new todo..."
/>
<button onClick={addTodo}>Add</button>
</div>
<ul className="todo-list">
{todos.map((todo) => (
<li key={todo.id} className={todo.completed ? 'completed' : ''}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => toggleTodo(todo.id)}
/>
<span>{todo.text}</span>
<button
className="delete-btn"
onClick={() => deleteTodo(todo.id)}
>
Delete
</button>
</li>
))}
</ul>
</div>
);
}
export default App;
Step 3: Add Some Basic Styling
Replace src/App.css with:
.app {
max-width: 600px;
margin: 50px auto;
font-family: Arial, sans-serif;
}
h1 {
color: #333;
text-align: center;
}
.input-container {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.input-container input {
flex: 1;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}
.input-container button {
padding: 10px 20px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
.input-container button:hover {
background-color: #0056b3;
}
.todo-list {
list-style: none;
padding: 0;
}
.todo-list li {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid #eee;
border-radius: 4px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.todo-list li.completed {
opacity: 0.6;
}
.todo-list li.completed span {
text-decoration: line-through;
color: #999;
}
.todo-list input[type="checkbox"] {
width: 20px;
height: 20px;
margin-right: 15px;
cursor: pointer;
}
.todo-list span {
flex: 1;
font-size: 16px;
}
.delete-btn {
padding: 5px 10px;
background-color: #dc3545;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.delete-btn:hover {
background-color: #c82333;
}
Step 4: Test Your App
Visit http://localhost:3000 and you’ll see your working todo app! Try adding todos, marking them complete, and deleting them. Everything works thanks to useState managing your component’s state.
Understanding the Todo App
Let’s understand what’s happening under the hood:
State Management:
-
todos— An array holding all your todo items. Each todo has anid,text, andcompletedstatus. -
input— Stores what the user is typing in the input field.
State Updates:
-
addTodo()— Creates a new todo object withDate.now()as a unique ID, adds it to the todos array using the spread operator[...todos, newTodo], and clears the input. -
toggleTodo(id)— Uses.map()to find the matching todo and flip itscompletedstatus while keeping others unchanged. -
deleteTodo(id)— Uses.filter()to remove the todo with the matching ID.
Why This Pattern Works:
Notice how we never modify state directly. Instead, we create new arrays and objects. This is crucial because React compares the old and new state to know when to re-render. If you mutated the state directly, React might not detect the change.
Important Rules When Using Hooks
Before you start building more complex apps, know these critical rules:
-
Only call Hooks at the top level — Don’t call Hooks inside loops, conditions, or nested functions. Always call them at the top level of your component.
// ✅ Correct
function MyComponent() {
const [count, setCount] = useState(0);
// ... rest of code
}
// ❌ Wrong
function MyComponent() {
if (someCondition) {
const [count, setCount] = useState(0); // Don't do this!
}
}
-
Only call Hooks from React functions — Call Hooks only from React functional components or custom Hooks, not from regular JavaScript functions.
Next Steps: Explore More Hooks
Once you’re comfortable with useState and useEffect, React has several other built-in Hooks worth exploring:
-
useContext— Share data between components without passing props at every level -
useReducer— Manage complex state logic similar to Redux -
useRef— Access DOM elements directly or keep values that don’t trigger re-renders -
useCallbackanduseMemo— Optimize performance by memoizing functions and values
Common Beginner Mistakes to Avoid
1. Forgetting the dependency array in useEffect
// This runs after EVERY render (probably not what you want)
useEffect(() => {
fetchData();
});
// This runs only once after mounting
useEffect(() => {
fetchData();
}, []);
// This runs when 'userId' changes
useEffect(() => {
fetchData(userId);
}, [userId]);
2. Mutating state directly
// ❌ Wrong
const [count, setCount] = useState(0);
count = count + 1; // Don't do this!
// ✅ Correct
const [count, setCount] = useState(0);
setCount(count + 1);
3. Creating new objects/arrays without reason
// If you have an object state
const [user, setUser] = useState({ name: 'John', age: 30 });
// ❌ Create new object unnecessarily
setUser({ name: 'John', age: 30 }); // This causes re-render even though nothing changed
// ✅ Only update what changed
setUser({ ...user, age: 31 });
Wrapping Up
React Hooks transformed the way developers write React components. With useState and useEffect, you can build fully functional apps without ever touching class components. The todo app you just built is a perfect starting point—it demonstrates state management, event handling, and component updates.
Read also: How to create a forever loop in JavaScript





