Lesson 1: Introduction to React Hooks
Learn the fundamentals of React Hooks, including useState and useEffect, to build modern functional components with state and side effects.
๐ฃ Introduction to React Hooks
Welcome to the world of React Hooks! If you've been learning React, you might have heard that Hooks are a game-changer. But what exactly are they, and why should you care? Let's dive in and discover how Hooks make your React code cleaner, simpler, and more powerful.
๐ค What Are React Hooks?
Imagine you're building with LEGO blocks. In the old days of React, you had two types of blocks: class components (big, complex blocks with lots of features) and functional components (small, simple blocks that could only display things). If you wanted to add features like memory (state) or timers (side effects) to your simple blocks, you had to rebuild them as complex blocks. That was frustrating!
React Hooks are special tools that let you add superpowers to your simple functional components without converting them into class components. Think of Hooks as magical attachments you can click onto your LEGO blocks to give them new abilities.
๐ก Key Point: Hooks are functions that let you "hook into" React features like state and lifecycle methods from functional components.
๐ฏ Why Were Hooks Invented?
Before Hooks (introduced in React 16.8 in February 2019), developers faced several challenges:
- Complex Class Components: Writing
this.state,this.setState, andthis.propseverywhere was tedious and error-prone - Confusing Lifecycle Methods: Logic was scattered across
componentDidMount,componentDidUpdate, andcomponentWillUnmount - Hard to Reuse Logic: Sharing stateful logic between components required complex patterns like "render props" or "higher-order components"
- Confusing
thisKeyword: Binding event handlers and understandingthisconfused many developers
Hooks solved all these problems! ๐
๐๏ธ The Architecture of Hooks
Let's visualize how Hooks fit into React:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Functional Component โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ useState Hook โ โ
โ โ (manages state) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ useEffect Hook โ โ
โ โ (handles side effects) โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ Other Hooks... โ โ
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ โ
โ โ
โ โ โ โ โ
โ Returns JSX โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ The Rules of Hooks
Before we dive deeper, you must understand these two golden rules:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ THE RULES OF HOOKS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Only call Hooks at the TOP LEVEL โ
โ โ Don't call inside loops โ
โ โ Don't call inside conditions โ
โ โ Don't call inside nested funcs โ
โ โ
โ 2. Only call Hooks from: โ
โ โ
React functional components โ
โ โ
Custom Hooks โ
โ โ Regular JavaScript functions โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ก Why these rules? React relies on the order Hooks are called to track state correctly. If you call Hooks conditionally, the order might change between renders, breaking your app!
๐จ Core Hook #1: useState
The useState Hook is your bread and butter. It lets functional components remember values between renders.
The Anatomy of useState
const [value, setValue] = useState(initialValue);
Let's break this down:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ const [count, setCount] = useState(0); โ
โ โ โ โ โ
โ โ โ โ โ
โ current function initial โ
โ value to update value โ
โ the value โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
What's happening?
useState(0)returns an array with two items- We use array destructuring to grab both items at once
- First item: the current state value
- Second item: a function to update that value
- The
0is the initial value when the component first renders
๐ง Memory Tip: Think "use-state" as "use some memory to remember this value"
How useState Works Behind the Scenes
When you call setCount(5), React:
- Updates the state value
- Schedules a re-render of your component
- On the next render,
countwill have the new value - Your component function runs again with the updated value
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Initial Render โ
โ count = 0 โ
โ User clicks button โ
โ โ โ
โ setCount(1) is called โ
โ โ โ
โ React schedules re-render โ
โ โ โ
โ Second Render โ
โ count = 1 โ
โ UI updates with new value โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โก Core Hook #2: useEffect
The useEffect Hook lets you perform side effects in functional components.
๐ค What's a side effect? Anything that affects something outside the component:
- Fetching data from an API
- Setting up subscriptions
- Manually changing the DOM
- Setting timers
- Logging to the console
The Anatomy of useEffect
useEffect(() => {
// Your side effect code here
return () => {
// Cleanup code (optional)
};
}, [dependencies]);
Breaking it down:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ useEffect(() => { โ
โ document.title = `Count: ${count}`; โ
โ }, [count]); โ
โ โ โ โ
โ effect dependency โ
โ function array โ
โ โ
โ "Run this effect whenever 'count' changes" โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The Dependency Array: The Brain of useEffect
The dependency array is crucial for understanding useEffect:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Dependency Array Behavior โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ useEffect(() => {...}, [a, b]) โ
โ โ Runs when 'a' or 'b' changes โ
โ โ
โ useEffect(() => {...}, []) โ
โ โ Runs ONCE after first render โ
โ (empty array = no dependencies) โ
โ โ
โ useEffect(() => {...}) โ
โ โ Runs after EVERY render โ
โ (no array = always run) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ก Critical Insight: The dependency array tells React "re-run this effect only if these values changed." This prevents unnecessary work and infinite loops!
๐ Detailed Examples with Explanations
Example 1: Counter with useState ๐ข
Let's build a simple counter to understand useState in action:
import React, { useState } from 'react';
function Counter() {
// Declare a state variable called 'count'
// Initialize it with 0
const [count, setCount] = useState(0);
// Function to increment the count
const increment = () => {
setCount(count + 1);
};
// Function to decrement the count
const decrement = () => {
setCount(count - 1);
};
// Function to reset to zero
const reset = () => {
setCount(0);
};
return (
<div>
<h1>Count: {count}</h1>
<button onClick={increment}>Increase +</button>
<button onClick={decrement}>Decrease -</button>
<button onClick={reset}>Reset</button>
</div>
);
}
export default Counter;
What's happening step-by-step:
- Line 4: We call
useState(0)which gives uscount(current value: 0) andsetCount(updater function) - Line 8: When increment is called,
setCount(count + 1)tells React "update count to current value + 1" - Line 13: Similarly for decrement
- Line 18: Reset sets count back to 0
- Line 24:
{count}displays the current value - Line 25-27: Each button has an
onClickthat calls our functions
๐ง Try this: What happens if you click increment 3 times quickly? React batches the updates efficiently!
Example 2: Document Title with useEffect ๐
Let's make the browser tab title show our count:
import React, { useState, useEffect } from 'react';
function CounterWithTitle() {
const [count, setCount] = useState(0);
// This effect runs whenever 'count' changes
useEffect(() => {
// Update the document title
document.title = `You clicked ${count} times`;
// Log to see when effect runs
console.log('Effect ran! Count is now:', count);
}, [count]); // Only re-run if count changes
return (
<div>
<h1>Count: {count}</h1>
<button onClick={() => setCount(count + 1)}>
Click me!
</button>
<p>Check the browser tab title!</p>
</div>
);
}
export default CounterWithTitle;
Flow of execution:
1. Component renders (count = 0)
โ Document title = "You clicked 0 times"
2. User clicks button
โ setCount(1) is called
3. React re-renders component (count = 1)
โ useEffect sees count changed from 0 to 1
โ Effect runs again
โ Document title = "You clicked 1 times"
Why use [count] in the dependency array?
- Without it: Effect runs after every render (wasteful!)
- With
[count]: Effect only runs when count actually changes (efficient!)
Example 3: Data Fetching with useEffect ๐
A real-world pattern: fetching data when the component loads:
import React, { useState, useEffect } from 'react';
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// Fetch data when component mounts
fetch('https://api.example.com/user/1')
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}, []); // Empty array = run once on mount
if (loading) return <p>Loading user data...</p>;
if (error) return <p>Error: {error}</p>;
if (!user) return <p>No user found</p>;
return (
<div>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
</div>
);
}
export default UserProfile;
Breaking down this pattern:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Three State Pattern โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. user: null โ holds fetched data โ
โ 2. loading: true โ tracks if fetching โ
โ 3. error: null โ captures errors โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Why [] for dependencies?
- Empty array means "run this effect once when component first appears"
- Perfect for fetching initial data
- Without
[], it would fetch on every render (infinite loop!)
๐ฏ Real-world tip: This pattern is so common that many developers use libraries like React Query or SWR to handle it automatically!
Example 4: Cleanup with useEffect ๐งน
Some effects need cleanup (timers, subscriptions, listeners). Here's how:
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
const [isRunning, setIsRunning] = useState(false);
useEffect(() => {
if (!isRunning) return; // Don't set up timer if not running
// Set up the timer
const intervalId = setInterval(() => {
setSeconds(s => s + 1); // Using functional update
}, 1000);
// Cleanup function: runs when component unmounts
// or before effect runs again
return () => {
clearInterval(intervalId);
console.log('Timer cleaned up!');
};
}, [isRunning]); // Re-run when isRunning changes
return (
<div>
<h1>Seconds: {seconds}</h1>
<button onClick={() => setIsRunning(!isRunning)}>
{isRunning ? 'Pause' : 'Start'}
</button>
<button onClick={() => setSeconds(0)}>Reset</button>
</div>
);
}
export default Timer;
Why cleanup matters:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Without cleanup: โ
โ Component unmounts โ timer keeps running โ
โ โ Memory leak! ๐ฅ โ
โ โ
โ With cleanup: โ
โ Component unmounts โ cleanup runs โ
โ โ clearInterval() stops timer โ
โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Notice setSeconds(s => s + 1)?
- This is a functional update
- Instead of using the current
secondsvalue, we use a function - React passes the current value as
s - Safer when updates depend on previous value
โ ๏ธ Common Mistakes and How to Avoid Them
Mistake 1: Forgetting the Dependency Array
โ WRONG:
useEffect(() => {
document.title = `Count: ${count}`;
}); // Missing dependency array!
// This runs after EVERY render, even when count doesn't change
โ
CORRECT:
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // Effect only runs when count changes
Mistake 2: Mutating State Directly
โ WRONG:
const [items, setItems] = useState([1, 2, 3]);
items.push(4); // Mutating the array directly!
setItems(items); // React won't detect the change
โ
CORRECT:
const [items, setItems] = useState([1, 2, 3]);
setItems([...items, 4]); // Create a new array
๐ก Why? React compares old and new state by reference. If you mutate the original, the reference stays the same, so React thinks nothing changed!
Mistake 3: Using Hooks Inside Conditions
โ WRONG:
function MyComponent() {
if (condition) {
const [count, setCount] = useState(0); // Hook inside condition!
}
}
โ
CORRECT:
function MyComponent() {
const [count, setCount] = useState(0); // Hook at top level
if (condition) {
// Use the state here
}
}
Mistake 4: Stale Closure in useEffect
โ PROBLEM:
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // Always uses count from first render!
}, 1000);
return () => clearInterval(timer);
}, []); // Empty array means 'count' is never updated
โ
SOLUTION 1 - Add dependency:
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1);
}, 1000);
return () => clearInterval(timer);
}, [count]); // Re-run effect when count changes
โ
SOLUTION 2 - Functional update:
useEffect(() => {
const timer = setInterval(() => {
setCount(c => c + 1); // Uses current value
}, 1000);
return () => clearInterval(timer);
}, []); // Can stay empty because we don't reference count
Mistake 5: Infinite Loop with useEffect
โ WRONG:
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData); // Updates data
}); // No dependency array = runs every render
// โ setData causes re-render
// โ effect runs again
// โ infinite loop! ๐ฅ
โ
CORRECT:
const [data, setData] = useState([]);
useEffect(() => {
fetch('/api/data')
.then(res => res.json())
.then(setData);
}, []); // Empty array = run once
๐ Key Takeaways
Let's solidify what you've learned:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ REACT HOOKS ESSENTIALS โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ 1. Hooks let functional components have โ
โ state and side effects โ
โ โ
โ 2. useState manages component state: โ
โ const [value, setValue] = useState(init) โ
โ โ
โ 3. useEffect handles side effects: โ
โ useEffect(() => {...}, [dependencies]) โ
โ โ
โ 4. Dependency array controls when effects โ
โ run: [], [var], or omitted โ
โ โ
โ 5. Return cleanup function from useEffect โ
โ to prevent memory leaks โ
โ โ
โ 6. Always follow the Rules of Hooks: โ
โ - Only at top level โ
โ - Only in React functions โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ง Mental Models to Remember
useState = Memory: Think of it as giving your component a memory bank. Each call to useState creates a slot in that memory.
useEffect = Synchronization: Think of it as keeping your component synchronized with the outside world (APIs, timers, DOM).
Dependency Array = Trigger: It's like a trigger that says "run this effect when these values change."
๐ง Practical Tips for Success
- Start with useState: Master it before diving into more complex Hooks
- Use ESLint: Install
eslint-plugin-react-hooksto catch Hook mistakes automatically - Name custom Hooks with 'use': If you create your own Hooks, start the name with "use" (e.g.,
useForm,useAuth) - One Hook per concern: Don't try to do everything in one useState or useEffect
- Think in dependencies: Ask "what values does this effect depend on?" to determine the dependency array
๐ Quick Reference Card
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ REACT HOOKS CHEAT SHEET โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ useState โ
โ const [state, setState] = useState(initial) โ
โ โข Returns current state and updater function โ
โ โข setState(newValue) updates and re-renders โ
โ โข setState(prev => prev + 1) functional update โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ useEffect โ
โ useEffect(() => { /* effect */ }, [deps]) โ
โ โข Runs after render โ
โ โข [deps]: run when deps change โ
โ โข []: run once on mount โ
โ โข no array: run every render โ
โ โข return () => {}: cleanup function โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Rules โ
โ 1. Call Hooks at top level only โ
โ 2. Call Hooks from React functions only โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
โ Common Patterns โ
โ โข Fetching: useEffect with empty [] โ
โ โข Timers: useEffect with cleanup โ
โ โข Derived state: Calculate from existing state โ
โ โข Form inputs: useState for each field โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
๐ Real-World Applications
E-commerce: useState for shopping cart items, useEffect to persist cart to localStorage
Social Media: useState for post likes, useEffect to fetch new posts every 30 seconds
Dashboard: useState for filters, useEffect to fetch data when filters change
Forms: useState for each input field, useEffect for validation
๐ฏ What's Next?
You've mastered the foundation! Here's what to explore next:
- useContext: Share data without passing props
- useReducer: Manage complex state logic
- useRef: Access DOM elements and persist values
- useMemo & useCallback: Optimize performance
- Custom Hooks: Create your own reusable Hooks
๐ Further Study
Official React Hooks Documentation: https://react.dev/reference/react - Comprehensive guide straight from the React team with interactive examples
Kent C. Dodds' Epic React: https://epicreact.dev/articles - In-depth articles about Hook patterns and best practices from a React expert
React Hooks Visualized: https://www.youtube.com/watch?v=1jWS7cCuUXw - Visual explanations of how Hooks work under the hood
Congratulations! ๐ You now understand the fundamentals of React Hooks. The key to mastery is practice, so start building components using useState and useEffect. Happy coding! ๐ปโจ