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:

  1. Functional Components: Simple, but could only display data (no state or lifecycle methods)
  2. 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 }
8
9 componentDidMount() {
10 document.title = `Count: ${this.state.count}`;
11 }
12
13 componentDidUpdate() {
14 document.title = `Count: ${this.state.count}`;
15 }
16
17 increment() {
18 this.setState({ count: this.state.count + 1 });
19 }
20
21 render() {
22 return (
23 <div>
24 <p>Count: {this.state.count}</p>
25 <button onClick={this.increment}>+</button>
26 </div>
27 );
28 }
29}
30
31// After Hooks: Functional Component (✅ Simple)
32import { useState, useEffect } from 'react';
33
34function Counter() {
35 const [count, setCount] = useState(0);
36
37 useEffect(() => {
38 document.title = `Count: ${count}`;
39 }, [count]);
40
41 return (
42 <div>
43 <p>Count: {count}</p>
44 <button onClick={() => setCount(count + 1)}>+</button>
45 </div>
46 );
47}
48
49// Custom Hook: Reusable Logic
50function useCounter(initial = 0) {
51 const [count, setCount] = useState(initial);
52
53 return {
54 count,
55 increment: () => setCount(c => c + 1),
56 decrement: () => setCount(c => c - 1),
57 reset: () => setCount(initial)
58 };
59}
60
61// Using the custom hook
62function App() {
63 const counter = useCounter(10);
64
65 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:

  1. The current state value
  2. 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';
2
3// Basic useState Examples
4function Counter() {
5 const [count, setCount] = useState(0);
6
7 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}
15
16// Multiple State Variables
17function UserForm() {
18 const [name, setName] = useState('');
19 const [email, setEmail] = useState('');
20 const [isSubscribed, setIsSubscribed] = useState(false);
21
22 const handleSubmit = (e) => {
23 e.preventDefault();
24 console.log({ name, email, isSubscribed });
25 };
26
27 return (
28 <form onSubmit={handleSubmit}>
29 <input
30 value={name}
31 onChange={(e) => setName(e.target.value)}
32 placeholder="Name"
33 />
34 <input
35 type="email"
36 value={email}
37 onChange={(e) => setEmail(e.target.value)}
38 placeholder="Email"
39 />
40 <label>
41 <input
42 type="checkbox"
43 checked={isSubscribed}
44 onChange={(e) => setIsSubscribed(e.target.checked)}
45 />
46 Subscribe to newsletter
47 </label>
48 <button type="submit">Submit</button>
49 </form>
50 );
51}
52
53// Array State
54function TodoList() {
55 const [todos, setTodos] = useState([]);
56 const [input, setInput] = useState('');
57
58 const addTodo = () => {
59 if (input.trim()) {
60 setTodos([...todos, { id: Date.now(), text: input }]);
61 setInput('');
62 }
63 };
64
65 const deleteTodo = (id) => {
66 setTodos(todos.filter(todo => todo.id !== id));
67 };
68
69 return (
70 <div>
71 <input
72 value={input}
73 onChange={(e) => setInput(e.target.value)}
74 onKeyPress={(e) => e.key === 'Enter' && addTodo()}
75 />
76 <button onClick={addTodo}>Add</button>
77
78 {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}
87
88// Object State
89function Settings() {
90 const [settings, setSettings] = useState({
91 theme: 'light',
92 fontSize: 16,
93 notifications: true
94 });
95
96 const updateSetting = (key, value) => {
97 setSettings(prev => ({
98 ...prev,
99 [key]: value
100 }));
101 };
102
103 return (
104 <div>
105 <select
106 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>
112
113 <input
114 type="range"
115 min="12"
116 max="24"
117 value={settings.fontSize}
118 onChange={(e) => updateSetting('fontSize', e.target.value)}
119 />
120
121 <label>
122 <input
123 type="checkbox"
124 checked={settings.notifications}
125 onChange={(e) => updateSetting('notifications', e.target.checked)}
126 />
127 Enable notifications
128 </label>
129
130 <pre>{JSON.stringify(settings, null, 2)}</pre>
131 </div>
132 );
133}
134
135// Lazy Initial State
136function ExpensiveComponent() {
137 // Function only runs once on mount
138 const [data] = useState(() => {
139 console.log('This runs only once!');
140 return Array.from({ length: 100 }, (_, i) => i);
141 });
142
143 return <div>Items: {data.length}</div>;
144}
145
146// Functional Updates (for dependent state)
147function DoubleCounter() {
148 const [count, setCount] = useState(0);
149
150 const incrementTwice = () => {
151 // ❌ Wrong: Both use same count value
152 // setCount(count + 1);
153 // setCount(count + 1);
154
155 // ✅ Correct: Each uses previous value
156 setCount(prev => prev + 1);
157 setCount(prev => prev + 1);
158 };
159
160 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

  1. Data fetching: Load data when component mounts
  2. Subscriptions: Set up listeners and clean them up
  3. Timers: Start intervals and clear them
  4. Document updates: Update title, add/remove classes
  5. 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';
2
3// Basic useEffect Patterns
4
5// 1. Run once on mount
6function WelcomeMessage() {
7 useEffect(() => {
8 console.log('Component mounted!');
9 return () => console.log('Component unmounting!');
10 }, []); // Empty array = run once
11
12 return <h1>Welcome!</h1>;
13}
14
15// 2. Run when dependencies change
16function DocumentTitle({ title }) {
17 useEffect(() => {
18 document.title = title;
19 }, [title]); // Run when title changes
20
21 return <h1>{title}</h1>;
22}
23
24// 3. Cleanup example (timer)
25function Timer() {
26 const [seconds, setSeconds] = useState(0);
27
28 useEffect(() => {
29 const interval = setInterval(() => {
30 setSeconds(s => s + 1);
31 }, 1000);
32
33 // Cleanup function
34 return () => clearInterval(interval);
35 }, []); // Run once
36
37 return <div>Timer: {seconds}s</div>;
38}
39
40// 4. Data fetching
41function UserProfile({ userId }) {
42 const [user, setUser] = useState(null);
43 const [loading, setLoading] = useState(true);
44
45 useEffect(() => {
46 setLoading(true);
47
48 fetch(`/api/users/${userId}`)
49 .then(res => res.json())
50 .then(data => {
51 setUser(data);
52 setLoading(false);
53 });
54 }, [userId]); // Refetch when userId changes
55
56 if (loading) return <div>Loading...</div>;
57 return <div>{user?.name}</div>;
58}
59
60// 5. Event listeners
61function MousePosition() {
62 const [position, setPosition] = useState({ x: 0, y: 0 });
63
64 useEffect(() => {
65 const handleMove = (e) => {
66 setPosition({ x: e.clientX, y: e.clientY });
67 };
68
69 window.addEventListener('mousemove', handleMove);
70
71 // Cleanup
72 return () => {
73 window.removeEventListener('mousemove', handleMove);
74 };
75 }, []);
76
77 return <div>Mouse: {position.x}, {position.y}</div>;
78}
79
80// 6. Multiple effects
81function Dashboard() {
82 const [user, setUser] = useState(null);
83 const [notifications, setNotifications] = useState([]);
84
85 // Effect 1: Fetch user
86 useEffect(() => {
87 fetch('/api/user')
88 .then(res => res.json())
89 .then(setUser);
90 }, []);
91
92 // Effect 2: Fetch notifications
93 useEffect(() => {
94 fetch('/api/notifications')
95 .then(res => res.json())
96 .then(setNotifications);
97 }, []);
98
99 // Effect 3: Update title
100 useEffect(() => {
101 if (user) {
102 document.title = `Dashboard - ${user.name}`;
103 }
104 }, [user]);
105
106 return (
107 <div>
108 <h1>Welcome {user?.name}</h1>
109 <p>{notifications.length} new notifications</p>
110 </div>
111 );
112}
113
114// Common Patterns
115
116// Debounced search
117function Search() {
118 const [query, setQuery] = useState('');
119 const [results, setResults] = useState([]);
120
121 useEffect(() => {
122 if (!query) {
123 setResults([]);
124 return;
125 }
126
127 const timeoutId = setTimeout(() => {
128 // Simulate API call
129 console.log('Searching for:', query);
130 setResults([`Result for "${query}"`]);
131 }, 500);
132
133 return () => clearTimeout(timeoutId);
134 }, [query]);
135
136 return (
137 <div>
138 <input
139 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}
149
150// Async in useEffect
151function AsyncExample({ id }) {
152 const [data, setData] = useState(null);
153
154 useEffect(() => {
155 let cancelled = false;
156
157 async function fetchData() {
158 try {
159 const response = await fetch(`/api/data/${id}`);
160 const json = await response.json();
161
162 if (!cancelled) {
163 setData(json);
164 }
165 } catch (error) {
166 console.error('Error:', error);
167 }
168 }
169
170 fetchData();
171
172 // Cleanup: prevent setting state on unmounted component
173 return () => {
174 cancelled = true;
175 };
176 }, [id]);
177
178 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

  1. Conditional hooks: Move the condition inside the hook
  2. Hooks in loops: Extract the logic to a separate component
  3. Hooks in event handlers: Move to useEffect or component body
  4. 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 Rules
2
3function BadExample({ show }) {
4 // ❌ Wrong: Conditional hook
5 if (show) {
6 const [count, setCount] = useState(0);
7 }
8
9 // ❌ Wrong: Hook in loop
10 for (let i = 0; i < 3; i++) {
11 const [value, setValue] = useState(i);
12 }
13
14 // ❌ Wrong: Hook after early return
15 if (!show) return null;
16 const [name, setName] = useState('');
17
18 // ❌ Wrong: Hook in event handler
19 const handleClick = () => {
20 const [clicks, setClicks] = useState(0);
21 };
22}
23
24// ✅ CORRECT: Following Hook Rules
25
26function GoodExample({ show }) {
27 // ✅ All hooks at the top
28 const [count, setCount] = useState(0);
29 const [name, setName] = useState('');
30 const [items, setItems] = useState([0, 1, 2]);
31
32 // ✅ Conditional logic inside hooks
33 useEffect(() => {
34 if (show) {
35 console.log('Visible!');
36 }
37 }, [show]);
38
39 // ✅ Early return after hooks
40 if (!show) return null;
41
42 return (
43 <div>
44 <p>Count: {count}</p>
45 <button onClick={() => setCount(count + 1)}>+</button>
46 </div>
47 );
48}
49
50// ✅ Dynamic lists: Create separate components
51function ItemList({ items }) {
52 return items.map(item => (
53 <Item key={item.id} data={item} />
54 ));
55}
56
57function Item({ data }) {
58 // Each item has its own hooks
59 const [selected, setSelected] = useState(false);
60
61 return (
62 <div onClick={() => setSelected(!selected)}>
63 {data.name} {selected && '✓'}
64 </div>
65 );
66}
67
68// ✅ Custom Hooks (must start with "use")
69function useCounter(initial = 0) {
70 const [count, setCount] = useState(initial);
71
72 return {
73 count,
74 increment: () => setCount(c => c + 1),
75 decrement: () => setCount(c => c - 1),
76 reset: () => setCount(initial)
77 };
78}
79
80function 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 });
89
90 useEffect(() => {
91 localStorage.setItem(key, JSON.stringify(value));
92 }, [key, value]);
93
94 return [value, setValue];
95}
96
97// Using custom hooks
98function App() {
99 const counter = useCounter(0);
100 const [name, setName] = useLocalStorage('name', '');
101
102 return (
103 <div>
104 <input
105 value={name}
106 onChange={(e) => setName(e.target.value)}
107 placeholder="Your name"
108 />
109
110 <p>Count: {counter.count}</p>
111 <button onClick={counter.increment}>+</button>
112 <button onClick={counter.reset}>Reset</button>
113 </div>
114 );
115}
116
117// ESLint Plugin Setup
118// npm install eslint-plugin-react-hooks --save-dev
119// .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*/
129
130// Remember:
131// 1. Only call hooks at the top level
132// 2. Only call hooks from React functions
133// 3. Custom hooks must start with "use"
134// 4. Same hooks in same order every render