What are React Hooks?
React Hooks are special functions that let you "hook into" React features from functional components. They allow you to use state and other React features without writing class components, making your code simpler, more reusable, and easier to test.
🎯 Why Hooks Were Introduced
- Simpler Code: No need for class components in most cases
- Reusable Logic: Extract stateful logic into custom hooks
- Better Performance: Easier optimization and no class binding issues
- Cleaner Side Effects: Better organization of component lifecycle logic
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
name: ''
};
}
componentDidMount() {
document.title = `Count: ${this.state.count}`;
}
componentDidUpdate() {
document.title = `Count: ${this.state.count}`;
}
incrementCount = () => {
this.setState(prevState => ({
count: prevState.count + 1
}));
};
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={this.incrementCount}>
Increment
</button>
<input
value={this.state.name}
onChange={(e) => this.setState({name: e.target.value})}
/>
</div>
);
}
}
import { useState, useEffect } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
// Combines componentDidMount and componentDidUpdate
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]);
const incrementCount = () => {
setCount(prevCount => prevCount + 1);
};
return (
<div>
<p>Count: {count}</p>
<button onClick={incrementCount}>
Increment
</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
}
useState Hook
The useState hook is the most commonly used hook. It allows you to add state to functional components. It returns an array with two elements: the current state value and a function to update it.
🔧 useState Syntax
import { useState } from 'react';
function StateExamples() {
// Basic state
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [isVisible, setIsVisible] = useState(false);
// Array state
const [items, setItems] = useState([]);
// Object state
const [user, setUser] = useState({
name: '',
email: '',
age: 0
});
// Function to update count
const increment = () => {
setCount(count + 1);
// Or use functional update for safety
setCount(prevCount => prevCount + 1);
};
// Adding item to array
const addItem = (newItem) => {
setItems(prevItems => [...prevItems, newItem]);
};
// Updating object state
const updateUser = (field, value) => {
setUser(prevUser => ({
...prevUser,
[field]: value
}));
};
return (
<div>
<h3>Count: {count}</h3>
<button onClick={increment}>Increment</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter name"
/>
<div>
<label>
<input
type="checkbox"
checked={isVisible}
onChange={(e) => setIsVisible(e.target.checked)}
/>
Show content
</label>
{isVisible && <p>This content is visible!</p>}
</div>
</div>
);
}
useEffect Hook
The useEffect hook lets you perform side effects in functional components. It serves the same purpose as componentDidMount, componentDidUpdate, and componentWillUnmount combined.
🔄 useEffect Patterns
import { useState, useEffect } from 'react';
function EffectExamples() {
const [count, setCount] = useState(0);
const [data, setData] = useState(null);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
// 1. Effect that runs on every render
useEffect(() => {
console.log('Component rendered');
});
// 2. Effect that runs only once (on mount)
useEffect(() => {
console.log('Component mounted');
document.title = 'My React App';
}, []); // Empty dependency array
// 3. Effect that runs when count changes
useEffect(() => {
document.title = `Count: ${count}`;
}, [count]); // Dependency array with count
// 4. Effect with cleanup (like componentWillUnmount)
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup function
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Runs once, cleanup on unmount
// 5. Data fetching effect
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch('/api/data');
const result = await response.json();
setData(result);
} catch (error) {
console.error('Error fetching data:', error);
}
};
fetchData();
}, []); // Fetch once on mount
// 6. Effect with multiple dependencies
useEffect(() => {
if (count > 0 && data) {
console.log('Both count and data are available');
}
}, [count, data]); // Runs when either count or data changes
return (
<div>
<h3>Count: {count}</h3>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
<p>Window width: {windowWidth}px</p>
{data ? (
<div>Data loaded: {JSON.stringify(data)}</div>
) : (
<div>Loading data...</div>
)}
</div>
);
}
useContext Hook
The useContext hook allows you to subscribe to React context without introducing nesting. It's perfect for sharing data across multiple components without prop drilling.
🌐 Context Pattern
import { createContext, useContext, useState } from 'react';
// 1. Create Context
const ThemeContext = createContext();
const UserContext = createContext();
// 2. Create Provider Component
function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prev => prev === 'light' ? 'dark' : 'light');
};
const value = {
theme,
toggleTheme
};
return (
<ThemeContext.Provider value={value}>
{children}
</ThemeContext.Provider>
);
}
function UserProvider({ children }) {
const [user, setUser] = useState({
name: 'John Doe',
email: 'john@example.com'
});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
// 3. Components that use Context
function Header() {
const { theme, toggleTheme } = useContext(ThemeContext);
const { user } = useContext(UserContext);
return (
<header className={`header ${theme}`}>
<h1>Welcome, {user.name}!</h1>
<button onClick={toggleTheme}>
Switch to {theme === 'light' ? 'dark' : 'light'} mode
</button>
</header>
);
}
function Profile() {
const { user, setUser } = useContext(UserContext);
const { theme } = useContext(ThemeContext);
const updateName = (newName) => {
setUser(prev => ({ ...prev, name: newName }));
};
return (
<div className={`profile ${theme}`}>
<h2>Profile</h2>
<p>Name: {user.name}</p>
<p>Email: {user.email}</p>
<input
value={user.name}
onChange={(e) => updateName(e.target.value)}
/>
</div>
);
}
// 4. Main App with Providers
function App() {
return (
<ThemeProvider>
<UserProvider>
<Header />
<Profile />
</UserProvider>
</ThemeProvider>
);
}
Custom Hooks
Custom hooks are JavaScript functions that start with "use" and call other hooks. They let you extract component logic into reusable functions.
🔧 Custom Hook Examples
import { useState, useEffect } from 'react';
// 1. useCounter - Reusable counter logic
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(prev => prev + 1);
const decrement = () => setCount(prev => prev - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}
// 2. useFetch - Reusable data fetching
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) {
throw new Error('Network response was not ok');
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
if (url) {
fetchData();
}
}, [url]);
return { data, loading, error };
}
// 3. useLocalStorage - Persist state in localStorage
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading localStorage:', error);
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error setting localStorage:', error);
}
};
return [storedValue, setValue];
}
// 4. useToggle - Simple toggle functionality
function useToggle(initialValue = false) {
const [value, setValue] = useState(initialValue);
const toggle = () => setValue(prev => !prev);
const setTrue = () => setValue(true);
const setFalse = () => setValue(false);
return [value, { toggle, setTrue, setFalse }];
}
// Using custom hooks in components
function CounterComponent() {
const { count, increment, decrement, reset } = useCounter(10);
return (
<div>
<h3>Count: {count}</h3>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
<button onClick={reset}>Reset</button>
</div>
);
}
function DataComponent() {
const { data, loading, error } = useFetch('/api/users');
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{data && data.map(user => (
<div key={user.id}>{user.name}</div>
))}
</div>
);
}
function PreferencesComponent() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [isVisible, { toggle }] = useToggle(false);
return (
<div>
<button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}>
Current theme: {theme}
</button>
<button onClick={toggle}>
{isVisible ? 'Hide' : 'Show'} Content
</button>
{isVisible && <p>This content is toggleable!</p>}
</div>
);
}
Rules of Hooks
React hooks have specific rules that must be followed to ensure they work correctly. These rules are enforced by the ESLint plugin for React hooks.
📋 Hook Rules
- Only call hooks at the top level - Never inside loops, conditions, or nested functions
- Only call hooks from React functions - From functional components or custom hooks
- Custom hooks must start with "use" - This tells React it's a hook
✅ Correct Usage
function GoodComponent() {
// ✅ Always at top level
const [count, setCount] = useState(0);
const [name, setName] = useState('');
useEffect(() => {
// ✅ Effect logic here
}, []);
// ✅ Custom hook call
const { data } = useFetch('/api/data');
return <div>{/* JSX */}</div>;
}
❌ Incorrect Usage
function BadComponent() {
// ❌ Don't call hooks conditionally
if (someCondition) {
const [count, setCount] = useState(0);
}
// ❌ Don't call hooks in loops
for (let i = 0; i < 10; i++) {
useEffect(() => {}, []);
}
// ❌ Don't call hooks in event handlers
const handleClick = () => {
const [data, setData] = useState(null);
};
return <div>{/* JSX */}</div>;
}
🔧 ESLint Plugin
Install the React hooks ESLint plugin to catch hook rule violations:
npm install eslint-plugin-react-hooks
// .eslintrc.js
{
"plugins": ["react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn"
}
}
Advanced Hook Patterns
🚀 useReducer for Complex State
import { useReducer } from 'react';
// Reducer function
const todoReducer = (state, action) => {
switch (action.type) {
case 'ADD_TODO':
return {
...state,
todos: [...state.todos, {
id: Date.now(),
text: action.payload,
completed: false
}]
};
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
)
};
case 'DELETE_TODO':
return {
...state,
todos: state.todos.filter(todo => todo.id !== action.payload)
};
case 'SET_FILTER':
return {
...state,
filter: action.payload
};
default:
return state;
}
};
function TodoApp() {
const initialState = {
todos: [],
filter: 'all'
};
const [state, dispatch] = useReducer(todoReducer, initialState);
const addTodo = (text) => {
dispatch({ type: 'ADD_TODO', payload: text });
};
const toggleTodo = (id) => {
dispatch({ type: 'TOGGLE_TODO', payload: id });
};
const deleteTodo = (id) => {
dispatch({ type: 'DELETE_TODO', payload: id });
};
return (
<div>
<TodoInput onAdd={addTodo} />
<TodoList
todos={state.todos}
onToggle={toggleTodo}
onDelete={deleteTodo}
/>
</div>
);
}
⚡ Performance Optimization Hooks
import { useState, useMemo, useCallback } from 'react';
function OptimizedComponent({ items }) {
const [filter, setFilter] = useState('');
const [sortBy, setSortBy] = useState('name');
// useMemo for expensive calculations
const filteredAndSortedItems = useMemo(() => {
console.log('Filtering and sorting items...');
let filtered = items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
return filtered.sort((a, b) => {
if (sortBy === 'name') {
return a.name.localeCompare(b.name);
} else if (sortBy === 'price') {
return a.price - b.price;
}
return 0;
});
}, [items, filter, sortBy]); // Only recalculate when these change
// useCallback for stable function references
const handleItemClick = useCallback((itemId) => {
console.log('Item clicked:', itemId);
// Do something with the item
}, []); // Function doesn't depend on any values
const handleFilterChange = useCallback((newFilter) => {
setFilter(newFilter);
}, []);
return (
<div>
<SearchInput
value={filter}
onChange={handleFilterChange}
/>
<SortSelector
value={sortBy}
onChange={setSortBy}
/>
<ItemList
items={filteredAndSortedItems}
onItemClick={handleItemClick}
/>
</div>
);
}
Hook Best Practices
✅ Do's
- Always follow the Rules of Hooks
- Use custom hooks to share logic
- Keep effects focused and specific
- Use dependency arrays correctly
- Extract complex state logic to useReducer
- Use ESLint plugin for hook validation
❌ Don'ts
- Don't call hooks conditionally
- Don't forget cleanup in useEffect
- Don't omit dependencies
- Don't use hooks in regular functions
- Don't over-optimize with useMemo/useCallback
- Don't nest hooks inside loops
🎯 When to Use Each Hook
- useState: Simple state management
- useEffect: Side effects and lifecycle
- useContext: Avoiding prop drilling
- useReducer: Complex state logic
- useMemo: Expensive calculations
- useCallback: Stable function references
🔄 Common Patterns
- Data fetching with loading states
- Form handling with validation
- Timer and interval management
- Event listener cleanup
- Local storage persistence
- Component communication
What's Next?
You now have a solid understanding of React Hooks and how they can make your functional components more powerful and reusable. Next, you'll learn about managing complex application state.
🚀 Continue Learning
- State Management - Context API, Redux, and global state patterns
- Routing - Building single-page applications with React Router
- Development Tools - Essential tools for React development
- Transition Guide - Complete migration strategies