React Hooks Tutorial: Master useState and useEffect in 2026
Master React Hooks with this comprehensive tutorial for beginners. Learn useState, useEffect, and custom hooks with real-world examples from 50+ production projects. Avoid common mistakes and write cleaner React code today.
React Hooks transformed how I write components - from 200 lines to just 50 lines of cleaner code
Why React Hooks Changed Everything for Me
Three years ago, I was writing class components for every single React project. Each component had lifecycle methods scattered everywhere. componentDidMount here, componentDidUpdate there, and don't even get me started on componentWillUnmount.
My components looked like this:
- 150-200 lines of code for simple features
- Confusing "this" keyword everywhere
- Duplicate logic across different lifecycle methods
- Harder to test and harder to understand
Then React 16.8 introduced Hooks. I was skeptical. "Why fix something that isn't broken?" I thought. But after converting my first component, I was shocked.
That 200-line class component became 50 lines with hooks. No more binding methods. No more confusing lifecycle logic. Just clean, readable code that made sense.
In this React hooks tutorial, I'll teach you everything I wish someone taught me three years ago. These aren't textbook examples - these are real patterns I use in production apps serving thousands of users daily.
What Are React Hooks? (Simple Explanation)
Think of React Hooks as special functions that let you "hook into" React features without writing class components.
Hooks let you use state and lifecycle features in functional components
Before hooks, you needed class components for:
- Managing component state (like user input, loading status)
- Running code after component renders (like fetching data)
- Accessing component lifecycle (mounting, updating, unmounting)
Hooks changed the game. Now functional components can do everything class components can - and they're easier to write, read, and maintain.
The Two Essential React Hooks Every Developer Must Know
Out of all React hooks, you'll use these two 90% of the time. Master these first, and you'll be writing production-ready React code.
useState Hook: Managing Component State the Modern Way
useState is the most important React hook. It lets you add state to functional components. Here's how it works in the real world.
Basic useState Example (Counter App)
Let's start with the classic counter example, then I'll show you how I actually use it in real projects.
Code:
import React, { useState } from 'react';
function Counter() {
// Declare state variable called "count" with initial value of 0
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
How useState Works:
- useState(0) - Creates a state variable with initial value 0
- [count, setCount] - Array destructuring gives us the value and updater function
- setCount(count + 1) - Updates the state, triggering a re-render
Real-World useState: Form Input Handling
Here's how I use useState in every contact form I build for clients:
import React, { useState } from 'react';
function ContactForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
message: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const handleSubmit = (e) => {
e.preventDefault();
console.log('Form submitted:', formData);
// Send data to API here
};
return (
<form onSubmit={handleSubmit}>
<input
name="name"
value={formData.name}
onChange={handleChange}
placeholder="Your Name"
/>
<input
name="email"
value={formData.email}
onChange={handleChange}
placeholder="Your Email"
/>
<textarea
name="message"
value={formData.message}
onChange={handleChange}
placeholder="Your Message"
/>
<button type="submit">Send</button>
</form>
);
}
Why This Pattern Works:
- Single state object for all form fields (cleaner than 3 separate useState calls)
- Spread operator (...formData) preserves other fields when updating
- Dynamic property names [e.target.name] reduce code duplication
Every form I build uses useState for controlled components
Common useState Mistakes (And How to Fix Them)
After reviewing 100+ code submissions, I see beginners make these mistakes constantly:
❌ WRONG: Directly Modifying State
const [user, setUser] = useState({ name: 'John' });
user.name = 'Jane'; // This won't trigger re-render!
✅ CORRECT: Always Use Setter Function
setUser({ ...user, name: 'Jane' });
❌ WRONG: Using Previous State Incorrectly
setCount(count + 1);
setCount(count + 1); // Still only adds 1, not 2!
✅ CORRECT: Use Functional Updates
setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1); // Now adds 2
useEffect Hook: Running Side Effects in React Components
useEffect is the second most important hook. It handles "side effects" - things that happen outside your component like fetching data, subscriptions, or manually changing the DOM.
Understanding useEffect with Real Examples
Think of useEffect as your replacement for componentDidMount, componentDidUpdate, and componentWillUnmount combined into one powerful hook.
Basic useEffect: Fetching Data from API
This is how I fetch data in 90% of my React projects:
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// This runs after component mounts
async function fetchUser() {
try {
setLoading(true);
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (err) {
setError('Failed to load user');
} finally {
setLoading(false);
}
}
fetchUser();
}, [userId]); // Re-run effect when userId changes
if (loading) return <p>Loading...</p>
if (error) return <p>{error}</p>
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Breaking Down useEffect:
- First argument - Function that runs after render
- Second argument [userId] - Dependency array (effect re-runs when userId changes)
- async/await - Handle promises cleanly
- Loading states - Show feedback while fetching
useEffect Dependency Array: The Most Confusing Part
This confuses everyone at first. Here's the simple explanation:
No dependency array - Runs after every render:
useEffect(() => {
console.log('Runs after every render');
});
Empty array [] - Runs once on mount:
useEffect(() => {
console.log('Runs once when component mounts');
}, []);
With dependencies - Runs when dependencies change:
useEffect(() => {
console.log('Runs when userId or theme changes');
}, [userId, theme]);
Understanding dependency arrays is crucial for mastering useEffect
Real-World useEffect: Document Title Updates
Here's a simple but powerful use case I implement in every dashboard:
import React, { useState, useEffect } from 'react';
function Dashboard() {
const [notifications, setNotifications] = useState(0);
useEffect(() => {
// Update browser tab title
document.title = notifications > 0
? `(${notifications}) Dashboard`
: 'Dashboard';
}, [notifications]);
return (
<div>
<h1>Dashboard</h1>
<p>You have {notifications} new notifications</p>
</div>
);
}
This updates the browser tab title whenever notifications change. Users can see notification counts even when the tab isn't active!
useEffect Cleanup: Preventing Memory Leaks
This is critical for subscriptions, timers, and event listeners. Always clean up after yourself:
useEffect(() => {
// Set up subscription
const subscription = subscribeToNotifications(userId);
// Cleanup function (runs before component unmounts)
return () => {
subscription.unsubscribe();
};
}, [userId]);
Real Example: Timer Cleanup
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(s => s + 1);
}, 1000);
// Cleanup: Clear interval when component unmounts
return () => clearInterval(interval);
}, []);
return <p>Seconds: {seconds}</p>;
}
Combining useState and useEffect: Real Production Example
Here's a complete search component I built for a client's e-commerce site. It combines both hooks perfectly:
import React, { useState, useEffect } from 'react';
function ProductSearch() {
const [searchQuery, setSearchQuery] = useState('');
const [products, setProducts] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
// Don't search if query is empty
if (!searchQuery) {
setProducts([]);
return;
}
// Debounce: Wait 500ms after user stops typing
const timeoutId = setTimeout(async () => {
setLoading(true);
try {
const response = await fetch(`/api/search?q=${searchQuery}`);
const data = await response.json();
setProducts(data);
} catch (error) {
console.error('Search failed:', error);
} finally {
setLoading(false);
}
}, 500);
// Cleanup: Cancel previous timeout if user keeps typing
return () => clearTimeout(timeoutId);
}, [searchQuery]);
return (
<div>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search products..."
/>
{loading && <p>Searching...</p>}
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
</div>
);
}
What Makes This Code Production-Ready:
- Debouncing - Waits for user to finish typing before searching (saves API calls)
- Cleanup function - Cancels old searches if user keeps typing
- Loading states - Shows "Searching..." feedback
- Error handling - Catches API failures gracefully
- Empty state handling - Clears results when search is empty
Custom React Hooks: Write Reusable Logic
After writing the same code 10 times, I learned to create custom hooks. They're just functions that use other hooks.
Custom Hook Example: useFetch
I use this in every project to eliminate duplicate data fetching code:
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
async function fetchData() {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage in any component:
function UserList() {
const { data: users, loading, error } = useFetch('/api/users');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error}</p>;
return (
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
This single custom hook eliminated 300+ lines of duplicate code across my projects!
Custom hooks let you extract component logic into reusable functions
Custom Hook: useLocalStorage
Persist state to localStorage with this reusable hook:
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use default
const [value, setValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
// Update localStorage when value changes
useEffect(() => {
try {
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
}, [key, value]);
return [value, setValue];
}
// Usage:
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return (
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
);
}
Now theme preference persists even after page refresh!
React Hooks Best Practices from 50+ Production Apps
These aren't theoretical guidelines - these are hard-learned lessons from shipping real applications.
1. Always Call Hooks at the Top Level
❌ WRONG: Hooks in Conditionals
if (user) {
useState(0); // DON'T DO THIS!
}
✅ CORRECT: Hooks at Top Level
const [count, setCount] = useState(0);
if (user) {
// Use count here
}
2. Name Custom Hooks with "use" Prefix
This signals to React (and other developers) that it's a hook:
// Good names:
useFetch()
useForm()
useAuth()
// Bad names:
fetchData() // Doesn't start with "use"
getUserInfo() // Doesn't follow convention
3. Keep Effects Focused on Single Responsibility
❌ WRONG: One Effect Doing Too Much
useEffect(() => {
fetchUser();
subscribeToNotifications();
updateAnalytics();
trackPageView();
}, []);
✅ CORRECT: Separate Effects for Separate Concerns
useEffect(() => {
fetchUser();
}, [userId]);
useEffect(() => {
const sub = subscribeToNotifications();
return () => sub.unsubscribe();
}, []);
useEffect(() => {
trackPageView(pathname);
}, [pathname]);
4. Optimize with useMemo and useCallback (When Needed)
Don't optimize prematurely, but when you have performance issues, these hooks help:
import { useMemo, useCallback } from 'react';
function ExpensiveComponent({ items }) {
// Memoize expensive calculations
const sortedItems = useMemo(() => {
return items.sort((a, b) => b.price - a.price);
}, [items]);
// Memoize callback functions
const handleClick = useCallback((id) => {
console.log('Clicked:', id);
}, []);
return <ItemList items={sortedItems} onClick={handleClick} />;
}
Common React Hooks Mistakes and Fixes
Mistake #1: Infinite Loops with useEffect
This crashed my app the first time I did it:
// ❌ INFINITE LOOP!
const [count, setCount] = useState(0);
useEffect(() => {
setCount(count + 1); // Triggers re-render → runs effect again → infinite loop!
});
Fix: Add dependency array
useEffect(() => {
setCount(count + 1);
}, []); // Runs only once on mount
Mistake #2: Forgetting to Clean Up Subscriptions
// ❌ MEMORY LEAK!
useEffect(() => {
const subscription = api.subscribe();
// No cleanup = subscription keeps running after unmount
}, []);
Fix: Return cleanup function
useEffect(() => {
const subscription = api.subscribe();
return () => subscription.unsubscribe(); // ✅ Clean up
}, []);
Mistake #3: Stale Closures in Event Handlers
// ❌ STALE VALUE!
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // Always uses initial value (0)!
}, 1000);
return () => clearInterval(interval);
}, []); // Empty array = count never updates
Fix: Use functional updates
setCount(prevCount => prevCount + 1); // ✅ Always uses latest value
React Hooks Cheat Sheet for Quick Reference
Essential Hooks Quick Guide
- useState - Add state to functional components
- useEffect - Run side effects (data fetching, subscriptions, DOM updates)
- useContext - Access context values without prop drilling
- useRef - Access DOM elements or persist values between renders
- useMemo - Memoize expensive calculations
- useCallback - Memoize callback functions
- useReducer - Manage complex state logic (alternative to useState)
When to Use Which Hook
- Simple state (string, number, boolean)? → useState
- Complex state (objects, arrays)? → useState or useReducer
- Fetch data on mount? → useEffect with empty dependency array
- Subscribe to events? → useEffect with cleanup
- Access DOM element? → useRef
- Share data across components? → useContext
- Performance issues? → useMemo or useCallback
Keep this guide handy while learning React hooks
Next Steps: Practice with Real Projects
Reading tutorials is great, but you'll only truly understand hooks by building real applications. Here are three projects I recommend:
Beginner Project: Todo App with Local Storage
Build a todo list that:
- Uses useState for managing todos
- Uses useEffect to save to localStorage
- Lets you add, complete, and delete tasks
Intermediate Project: Weather App with API
Create a weather dashboard that:
- Fetches data from OpenWeather API using useEffect
- Shows loading and error states
- Implements search with debouncing
- Uses custom useFetch hook
Advanced Project: Real-Time Chat Application
Build a chat app with:
- WebSocket subscriptions in useEffect
- useReducer for complex message state
- useContext for user authentication
- Custom hooks for chat logic
Frequently Asked Questions About React Hooks
Can I use hooks in class components?
No, hooks only work in functional components. If you have class components, you'll need to convert them to functional components first, or keep using class-based lifecycle methods.
Do I need to know class components before learning hooks?
No! If you're learning React in 2026, start with hooks. Class components are legacy knowledge at this point. All modern React development uses hooks.
What's the difference between useEffect and useLayoutEffect?
useEffect runs after browser paint (asynchronous). useLayoutEffect runs before browser paint (synchronous). Use useEffect 99% of the time. Only use useLayoutEffect when you need to measure or modify DOM before users see it.
How many useState calls is too many?
If you have more than 5-7 useState calls in one component, consider:
- Combining related state into objects
- Using useReducer for complex state logic
- Breaking component into smaller components
Can I make async functions in useEffect?
Not directly. useEffect can't be async, but you can call async functions inside it:
// ❌ WRONG
useEffect(async () => {
await fetchData();
}, []);
// ✅ CORRECT
useEffect(() => {
async function loadData() {
await fetchData();
}
loadData();
}, []);
Resources to Master React Hooks
These are the resources that helped me master hooks:
Official Documentation
- React Hooks Documentation - Start here for official explanations
- React Hooks FAQ - Answers to common questions
- React Hooks API Reference - Complete API documentation
Practice Platforms
- CodeSandbox - Practice hooks in browser without setup
- React Challenges - Build small projects with hooks
- Frontend Mentor - Real-world design challenges using React
Communities
- React Discord - Ask questions and get help from experienced developers
- r/reactjs Reddit - Share projects and learn from others
- Stack Overflow - Search for specific hook-related problems
Final Thoughts: Your React Hooks Journey Starts Now
Three years ago, I struggled with React class components. Today, I build production apps with hooks that serve thousands of users daily. The difference? Practice and patience.
Don't try to memorize everything from this React hooks tutorial. Bookmark this page, come back when you need it, and most importantly - start building.
Your first component with hooks might feel awkward. That's normal. By your tenth component, it'll feel natural. By your hundredth, you'll wonder how you ever lived without them.
Start small: Build a counter. Then a todo list. Then fetch some data from an API. Each small project builds your confidence and skills.
Remember: Every expert React developer started exactly where you are now. The only difference is they kept coding when it got hard.
Now go build something amazing with React hooks. Your future self will thank you.
Your first React hooks project is just the beginning of an exciting journey
Want to Learn More?
This is just the beginning of your React journey. Check out my other tutorials on building production-ready applications:
- How I Build Fast Websites with Next.js
- Deploy React Apps to Production: Complete Guide
- JavaScript Best Practices from Real Projects
Have questions about React hooks? Reach out on my contact page. I read every message and reply to as many as I can!