React Hooks Introduction
Learn about React Hooks and how they simplify component logic.
What are React Hooks? The Revolution in React Development
React Hooks represent one of the most significant advances in React development since the library's inception. Think of Hooks as a set of special functions that let you "hook into" React's internal features from functional components. They were introduced in React 16.8 and fundamentally changed how we write React applications.
The Problem Hooks Solved Before Hooks, React had two types of components:
- Functional Components: Simple, but could only display data (no state or lifecycle methods)
- Class Components: Powerful, but complex with confusing
this
binding and verbose syntax
This created a frustrating developer experience where you'd often start with a simple functional component, then need to convert it to a class component just to add state or lifecycle methods.
What Are Hooks Really? Hooks are JavaScript functions with special rules. They allow you to use React features like state and lifecycle methods in functional components. The name "hooks" comes from the idea that they let you "hook into" React's state and lifecycle features.
The Revolutionary Impact Hooks eliminated the need for class components in most cases, making React code:
- Simpler: No more
this
binding confusion - More reusable: Logic can be easily shared between components
- Easier to test: Functional components are simpler to test
- Smaller: Less boilerplate code means smaller bundle sizes
- More powerful: Complex logic can be composed from simpler hooks
Built-in Hooks vs Custom Hooks
React provides several built-in hooks (like useState
, useEffect
, useContext
), but you can also create custom hooks to share logic between components. Custom hooks are just regular JavaScript functions that use other hooks.
The Learning Curve While hooks make React development easier overall, they do require learning new patterns and ways of thinking. The key is understanding that hooks run on every render and follow specific rules.
Real-World Analogy Think of hooks like power tools in a workshop. Before hooks, you had basic hand tools (functional components) for simple tasks and complex machinery (class components) for advanced work. Hooks are like having a modular power tool system where you can attach different bits (hooks) to handle various tasks with the same simple, consistent interface.
1// Before Hooks: Class Component (❌ Complex)2class Counter extends React.Component {3 constructor(props) {4 super(props);5 this.state = { count: 0 };6 this.increment = this.increment.bind(this); // Binding required!7 }89 componentDidMount() {10 document.title = `Count: ${this.state.count}`;11 }1213 componentDidUpdate() {14 document.title = `Count: ${this.state.count}`;15 }1617 increment() {18 this.setState({ count: this.state.count + 1 });19 }2021 render() {22 return (23 <div>24 <p>Count: {this.state.count}</p>25 <button onClick={this.increment}>+</button>26 </div>27 );28 }29}3031// After Hooks: Functional Component (✅ Simple)32import { useState, useEffect } from 'react';3334function Counter() {35 const [count, setCount] = useState(0);3637 useEffect(() => {38 document.title = `Count: ${count}`;39 }, [count]);4041 return (42 <div>43 <p>Count: {count}</p>44 <button onClick={() => setCount(count + 1)}>+</button>45 </div>46 );47}4849// Custom Hook: Reusable Logic50function useCounter(initial = 0) {51 const [count, setCount] = useState(initial);5253 return {54 count,55 increment: () => setCount(c => c + 1),56 decrement: () => setCount(c => c - 1),57 reset: () => setCount(initial)58 };59}6061// Using the custom hook62function App() {63 const counter = useCounter(10);6465 return (66 <div>67 <p>Count: {counter.count}</p>68 <button onClick={counter.increment}>+</button>69 <button onClick={counter.decrement}>-</button>70 <button onClick={counter.reset}>Reset</button>71 </div>72 );73}
useState Hook: Managing Component State Made Simple
The useState
hook is your gateway to adding state to functional components. It's the most commonly used hook and the first one most developers learn. Understanding useState
is crucial because it forms the foundation for building interactive React applications.
What is useState?
useState
is a function that allows you to add state variables to functional components. It takes an initial value and returns an array with two elements:
- The current state value
- A function to update that value
Why useState is Revolutionary
Before hooks, adding state to a component meant converting it from a functional component to a class component. This was verbose and often overkill for simple state needs. useState
eliminates this complexity.
Understanding the Array Destructuring Pattern
useState
returns an array, and we use array destructuring to get the values:
const [state, setState] = useState(initialValue)
You can name these variables anything you want, but the convention is:
- First element: descriptive name for the state value
- Second element: "set" + the state name (e.g.,
setCount
,setName
)
State Updates are Asynchronous When you call a state setter function, React doesn't update the state immediately. Instead, it schedules the update and re-renders the component. This is important to understand for avoiding common bugs.
Functional Updates When the new state depends on the previous state, use the functional update pattern to avoid stale closures and race conditions.
State Initialization
You can pass a value or a function to useState
. If initialization is expensive, pass a function to avoid recalculating on every render.
Multiple State Variables vs Object State
You can use multiple useState
calls for separate state variables, or use a single useState
with an object. Both approaches have merits depending on how the data is related.
1import { useState } from 'react';23// Basic useState Examples4function Counter() {5 const [count, setCount] = useState(0);67 return (8 <div>9 <p>Count: {count}</p>10 <button onClick={() => setCount(count + 1)}>+</button>11 <button onClick={() => setCount(prev => prev - 1)}>-</button>12 </div>13 );14}1516// Multiple State Variables17function UserForm() {18 const [name, setName] = useState('');19 const [email, setEmail] = useState('');20 const [isSubscribed, setIsSubscribed] = useState(false);2122 const handleSubmit = (e) => {23 e.preventDefault();24 console.log({ name, email, isSubscribed });25 };2627 return (28 <form onSubmit={handleSubmit}>29 <input30 value={name}31 onChange={(e) => setName(e.target.value)}32 placeholder="Name"33 />34 <input35 type="email"36 value={email}37 onChange={(e) => setEmail(e.target.value)}38 placeholder="Email"39 />40 <label>41 <input42 type="checkbox"43 checked={isSubscribed}44 onChange={(e) => setIsSubscribed(e.target.checked)}45 />46 Subscribe to newsletter47 </label>48 <button type="submit">Submit</button>49 </form>50 );51}5253// Array State54function TodoList() {55 const [todos, setTodos] = useState([]);56 const [input, setInput] = useState('');5758 const addTodo = () => {59 if (input.trim()) {60 setTodos([...todos, { id: Date.now(), text: input }]);61 setInput('');62 }63 };6465 const deleteTodo = (id) => {66 setTodos(todos.filter(todo => todo.id !== id));67 };6869 return (70 <div>71 <input72 value={input}73 onChange={(e) => setInput(e.target.value)}74 onKeyPress={(e) => e.key === 'Enter' && addTodo()}75 />76 <button onClick={addTodo}>Add</button>7778 {todos.map(todo => (79 <div key={todo.id}>80 {todo.text}81 <button onClick={() => deleteTodo(todo.id)}>×</button>82 </div>83 ))}84 </div>85 );86}8788// Object State89function Settings() {90 const [settings, setSettings] = useState({91 theme: 'light',92 fontSize: 16,93 notifications: true94 });9596 const updateSetting = (key, value) => {97 setSettings(prev => ({98 ...prev,99 [key]: value100 }));101 };102103 return (104 <div>105 <select106 value={settings.theme}107 onChange={(e) => updateSetting('theme', e.target.value)}108 >109 <option value="light">Light</option>110 <option value="dark">Dark</option>111 </select>112113 <input114 type="range"115 min="12"116 max="24"117 value={settings.fontSize}118 onChange={(e) => updateSetting('fontSize', e.target.value)}119 />120121 <label>122 <input123 type="checkbox"124 checked={settings.notifications}125 onChange={(e) => updateSetting('notifications', e.target.checked)}126 />127 Enable notifications128 </label>129130 <pre>{JSON.stringify(settings, null, 2)}</pre>131 </div>132 );133}134135// Lazy Initial State136function ExpensiveComponent() {137 // Function only runs once on mount138 const [data] = useState(() => {139 console.log('This runs only once!');140 return Array.from({ length: 100 }, (_, i) => i);141 });142143 return <div>Items: {data.length}</div>;144}145146// Functional Updates (for dependent state)147function DoubleCounter() {148 const [count, setCount] = useState(0);149150 const incrementTwice = () => {151 // ❌ Wrong: Both use same count value152 // setCount(count + 1);153 // setCount(count + 1);154155 // ✅ Correct: Each uses previous value156 setCount(prev => prev + 1);157 setCount(prev => prev + 1);158 };159160 return (161 <button onClick={incrementTwice}>162 Count: {count} (Click to +2)163 </button>164 );165}
useEffect Hook: Mastering Side Effects and Lifecycle
The useEffect
hook is where React's functional components truly shine. It's your tool for handling side effects - operations that affect things outside of the component's render function. Think of useEffect
as a combination of three class component lifecycle methods: componentDidMount
, componentDidUpdate
, and componentWillUnmount
.
What Are Side Effects? Side effects are operations that interact with the "outside world" beyond just rendering UI:
- Making API calls
- Setting up subscriptions
- Manually changing the DOM
- Starting/stopping timers
- Logging to the console
- Updating the document title
Understanding useEffect's Behavior
useEffect
runs after every render by default. This is different from class component lifecycle methods and is a key concept to understand. React applies effects in the order they appear in your component.
The Dependency Array: Controlling When Effects Run
The dependency array is the second argument to useEffect
and controls when the effect runs:
- No dependency array: Effect runs after every render
- Empty dependency array []: Effect runs only once (on mount)
- Array with dependencies: Effect runs when any dependency changes
Cleanup Functions: Preventing Memory Leaks Effects can return a cleanup function that React will call before running the effect again or when the component unmounts. This is crucial for preventing memory leaks.
Common useEffect Patterns
- Data fetching: Load data when component mounts
- Subscriptions: Set up listeners and clean them up
- Timers: Start intervals and clear them
- Document updates: Update title, add/remove classes
- Conditional effects: Run effects based on state changes
Multiple useEffect Hooks
You can use multiple useEffect
hooks in a single component to separate concerns. Each effect handles a specific piece of functionality.
1import { useState, useEffect } from 'react';23// Basic useEffect Patterns45// 1. Run once on mount6function WelcomeMessage() {7 useEffect(() => {8 console.log('Component mounted!');9 return () => console.log('Component unmounting!');10 }, []); // Empty array = run once1112 return <h1>Welcome!</h1>;13}1415// 2. Run when dependencies change16function DocumentTitle({ title }) {17 useEffect(() => {18 document.title = title;19 }, [title]); // Run when title changes2021 return <h1>{title}</h1>;22}2324// 3. Cleanup example (timer)25function Timer() {26 const [seconds, setSeconds] = useState(0);2728 useEffect(() => {29 const interval = setInterval(() => {30 setSeconds(s => s + 1);31 }, 1000);3233 // Cleanup function34 return () => clearInterval(interval);35 }, []); // Run once3637 return <div>Timer: {seconds}s</div>;38}3940// 4. Data fetching41function UserProfile({ userId }) {42 const [user, setUser] = useState(null);43 const [loading, setLoading] = useState(true);4445 useEffect(() => {46 setLoading(true);4748 fetch(`/api/users/${userId}`)49 .then(res => res.json())50 .then(data => {51 setUser(data);52 setLoading(false);53 });54 }, [userId]); // Refetch when userId changes5556 if (loading) return <div>Loading...</div>;57 return <div>{user?.name}</div>;58}5960// 5. Event listeners61function MousePosition() {62 const [position, setPosition] = useState({ x: 0, y: 0 });6364 useEffect(() => {65 const handleMove = (e) => {66 setPosition({ x: e.clientX, y: e.clientY });67 };6869 window.addEventListener('mousemove', handleMove);7071 // Cleanup72 return () => {73 window.removeEventListener('mousemove', handleMove);74 };75 }, []);7677 return <div>Mouse: {position.x}, {position.y}</div>;78}7980// 6. Multiple effects81function Dashboard() {82 const [user, setUser] = useState(null);83 const [notifications, setNotifications] = useState([]);8485 // Effect 1: Fetch user86 useEffect(() => {87 fetch('/api/user')88 .then(res => res.json())89 .then(setUser);90 }, []);9192 // Effect 2: Fetch notifications93 useEffect(() => {94 fetch('/api/notifications')95 .then(res => res.json())96 .then(setNotifications);97 }, []);9899 // Effect 3: Update title100 useEffect(() => {101 if (user) {102 document.title = `Dashboard - ${user.name}`;103 }104 }, [user]);105106 return (107 <div>108 <h1>Welcome {user?.name}</h1>109 <p>{notifications.length} new notifications</p>110 </div>111 );112}113114// Common Patterns115116// Debounced search117function Search() {118 const [query, setQuery] = useState('');119 const [results, setResults] = useState([]);120121 useEffect(() => {122 if (!query) {123 setResults([]);124 return;125 }126127 const timeoutId = setTimeout(() => {128 // Simulate API call129 console.log('Searching for:', query);130 setResults([`Result for "${query}"`]);131 }, 500);132133 return () => clearTimeout(timeoutId);134 }, [query]);135136 return (137 <div>138 <input139 value={query}140 onChange={(e) => setQuery(e.target.value)}141 placeholder="Search..."142 />143 {results.map((result, i) => (144 <div key={i}>{result}</div>145 ))}146 </div>147 );148}149150// Async in useEffect151function AsyncExample({ id }) {152 const [data, setData] = useState(null);153154 useEffect(() => {155 let cancelled = false;156157 async function fetchData() {158 try {159 const response = await fetch(`/api/data/${id}`);160 const json = await response.json();161162 if (!cancelled) {163 setData(json);164 }165 } catch (error) {166 console.error('Error:', error);167 }168 }169170 fetchData();171172 // Cleanup: prevent setting state on unmounted component173 return () => {174 cancelled = true;175 };176 }, [id]);177178 return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>;179}
Rules of Hooks: The Foundation of Hook Reliability
React Hooks follow specific rules that ensure they work correctly and predictably. These rules aren't arbitrary - they're fundamental to how React tracks and manages hook state between renders. Understanding and following these rules is crucial for building reliable React applications.
Why Do These Rules Exist? React uses the order of hook calls to associate each hook with its corresponding state between renders. React doesn't know which hook is which by name - it relies entirely on the order they're called. Breaking these rules can lead to bugs, crashes, and unpredictable behavior.
Rule 1: Only Call Hooks at the Top Level Never call hooks inside loops, conditions, or nested functions. This ensures that hooks are always called in the same order every time the component renders.
Rule 2: Only Call Hooks from React Functions Only call hooks from:
- React functional components
- Custom hooks (functions that start with "use")
Don't call hooks from:
- Regular JavaScript functions
- Class components
- Event handlers
- useEffect cleanup functions
Rule 3: Custom Hooks Must Start with "use" This is a convention that helps React and developer tools identify custom hooks. It also enables the linting rules to work properly.
The ESLint Plugin: Your Safety Net
React provides an ESLint plugin (eslint-plugin-react-hooks
) that automatically catches violations of these rules. Always use this plugin in your projects.
How React Tracks Hooks Internally React maintains a list of hooks for each component instance. On each render, React goes through this list in order and gives you the current value for each hook. If the order changes, React gets confused about which hook is which.
Common Violations and How to Fix Them
- Conditional hooks: Move the condition inside the hook
- Hooks in loops: Extract the logic to a separate component
- Hooks in event handlers: Move to useEffect or component body
- Early returns before hooks: Move hooks above early returns
Real-World Debugging When hook rules are violated, you'll often see errors like:
- "Hooks can only be called inside the body of a function component"
- "Hook was called more times than during the previous render"
- Hooks returning stale or incorrect values
1// ❌ WRONG: Breaking Hook Rules23function BadExample({ show }) {4 // ❌ Wrong: Conditional hook5 if (show) {6 const [count, setCount] = useState(0);7 }89 // ❌ Wrong: Hook in loop10 for (let i = 0; i < 3; i++) {11 const [value, setValue] = useState(i);12 }1314 // ❌ Wrong: Hook after early return15 if (!show) return null;16 const [name, setName] = useState('');1718 // ❌ Wrong: Hook in event handler19 const handleClick = () => {20 const [clicks, setClicks] = useState(0);21 };22}2324// ✅ CORRECT: Following Hook Rules2526function GoodExample({ show }) {27 // ✅ All hooks at the top28 const [count, setCount] = useState(0);29 const [name, setName] = useState('');30 const [items, setItems] = useState([0, 1, 2]);3132 // ✅ Conditional logic inside hooks33 useEffect(() => {34 if (show) {35 console.log('Visible!');36 }37 }, [show]);3839 // ✅ Early return after hooks40 if (!show) return null;4142 return (43 <div>44 <p>Count: {count}</p>45 <button onClick={() => setCount(count + 1)}>+</button>46 </div>47 );48}4950// ✅ Dynamic lists: Create separate components51function ItemList({ items }) {52 return items.map(item => (53 <Item key={item.id} data={item} />54 ));55}5657function Item({ data }) {58 // Each item has its own hooks59 const [selected, setSelected] = useState(false);6061 return (62 <div onClick={() => setSelected(!selected)}>63 {data.name} {selected && '✓'}64 </div>65 );66}6768// ✅ Custom Hooks (must start with "use")69function useCounter(initial = 0) {70 const [count, setCount] = useState(initial);7172 return {73 count,74 increment: () => setCount(c => c + 1),75 decrement: () => setCount(c => c - 1),76 reset: () => setCount(initial)77 };78}7980function useLocalStorage(key, defaultValue) {81 const [value, setValue] = useState(() => {82 try {83 const item = localStorage.getItem(key);84 return item ? JSON.parse(item) : defaultValue;85 } catch {86 return defaultValue;87 }88 });8990 useEffect(() => {91 localStorage.setItem(key, JSON.stringify(value));92 }, [key, value]);9394 return [value, setValue];95}9697// Using custom hooks98function App() {99 const counter = useCounter(0);100 const [name, setName] = useLocalStorage('name', '');101102 return (103 <div>104 <input105 value={name}106 onChange={(e) => setName(e.target.value)}107 placeholder="Your name"108 />109110 <p>Count: {counter.count}</p>111 <button onClick={counter.increment}>+</button>112 <button onClick={counter.reset}>Reset</button>113 </div>114 );115}116117// ESLint Plugin Setup118// npm install eslint-plugin-react-hooks --save-dev119// .eslintrc.json:120/*121{122 "plugins": ["react-hooks"],123 "rules": {124 "react-hooks/rules-of-hooks": "error",125 "react-hooks/exhaustive-deps": "warn"126 }127}128*/129130// Remember:131// 1. Only call hooks at the top level132// 2. Only call hooks from React functions133// 3. Custom hooks must start with "use"134// 4. Same hooks in same order every render