Introduction to React Hooks
React Hooks were introduced in React 16.8 as a way to use state and lifecycle methods in functional components. They allow you to "hook into" React features without writing class components.
Hooks solve many problems that developers faced with class components, including complex lifecycle logic, sharing stateful logic between components, and the confusion around this
binding.
useState Hook
The useState
hook allows you to add state to functional components.
Basic Usage
import React, { useState } from 'react';
function Counter() {
// Declare state variable with initial value
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
export default Counter;
Multiple State Variables
function UserProfile() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [age, setAge] = useState(0);
const handleSubmit = (e) => {
e.preventDefault();
console.log({ name, email, age });
};
return (
<form onSubmit={handleSubmit}>
<input
type="text"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="number"
placeholder="Age"
value={age}
onChange={(e) => setAge(Number(e.target.value))}
/>
<button type="submit">Submit</button>
</form>
);
}
Object and Array State
function TodoList() {
const [todos, setTodos] = useState([]);
const [inputValue, setInputValue] = useState('');
const addTodo = () => {
if (inputValue.trim()) {
setTodos([...todos, {
id: Date.now(),
text: inputValue,
completed: false
}]);
setInputValue('');
}
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
return (
<div>
<div>
<input
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
placeholder="Add a todo"
/>
<button onClick={addTodo}>Add</button>
</div>
<ul>
{todos.map(todo => (
<li key={todo.id}>
<span
style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}
onClick={() => toggleTodo(todo.id)}
>
{todo.text}
</span>
<button onClick={() => deleteTodo(todo.id)}>Delete</button>
</li>
))}
</ul>
</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.
Basic useEffect
import React, { useState, useEffect } from 'react';
function DocumentTitle() {
const [count, setCount] = useState(0);
// Effect runs after every render
useEffect(() => {
document.title = `Count: ${count}`;
});
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
Increment
</button>
</div>
);
}
useEffect with Dependencies
function UserData({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Error fetching user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Only re-run when userId changes
if (loading) return <div>Loading...</div>;
if (!user) return <div>User not found</div>;
return (
<div>
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
);
}
Cleanup Functions
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Cleanup function
return () => {
clearInterval(interval);
};
}, []); // Empty dependency array = runs once on mount
return <div>Seconds: {seconds}</div>;
}
function WindowWidth() {
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize);
// Cleanup
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <div>Window width: {windowWidth}px</div>;
}
Custom Hooks
Custom hooks let you extract component logic into reusable functions. A custom hook is a JavaScript function whose name starts with "use" and that may call other hooks.
useLocalStorage Hook
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
// Get initial value from localStorage or use provided initial value
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error('Error reading from localStorage:', error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that persists
const setValue = (value) => {
try {
// Allow value to be a function so we have the same API as useState
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
};
return [storedValue, setValue];
}
// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const [language, setLanguage] = useLocalStorage('language', 'en');
return (
<div>
<select value={theme} onChange={(e) => setTheme(e.target.value)}>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<select value={language} onChange={(e) => setLanguage(e.target.value)}>
<option value="en">English</option>
<option value="es">Spanish</option>
</select>
</div>
);
}
useFetch Hook
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
// Usage
function PostList() {
const { data: posts, loading, error } = useFetch('/api/posts');
if (loading) return <div>Loading posts...</div>;
if (error) return <div>Error: {error}</div>;
return (
<div>
{posts?.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
);
}
Best Practices
Rules of Hooks
- Only call hooks at the top level of your React functions
- Don't call hooks inside loops, conditions, or nested functions
- Only call hooks from React function components or custom hooks
- Custom hook names should start with "use"
Performance Optimization
import React, { useState, useEffect, useMemo, useCallback } from 'react';
function ExpensiveComponent({ items, onItemClick }) {
const [filter, setFilter] = useState('');
// Memoize expensive calculations
const filteredItems = useMemo(() => {
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]);
// Memoize callback functions
const handleItemClick = useCallback((item) => {
onItemClick(item);
}, [onItemClick]);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter items..."
/>
{filteredItems.map(item => (
<div key={item.id} onClick={() => handleItemClick(item)}>
{item.name}
</div>
))}
</div>
);
}
Common Pitfalls
// ❌ Don't do this - stale closure
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // This will always use the initial count value
}, 1000);
return () => clearInterval(interval);
}, []); // Empty deps array causes stale closure
return <div>{count}</div>;
}
// ✅ Do this instead
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prevCount => prevCount + 1); // Use functional update
}, 1000);
return () => clearInterval(interval);
}, []); // Now it's safe to use empty deps
return <div>{count}</div>;
}
// ❌ Don't do this - infinite loop
function BadEffect() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}); // No dependency array causes infinite loop
return <div>{user?.name}</div>;
}
// ✅ Do this instead
function GoodEffect() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []); // Empty array means run once on mount
return <div>{user?.name}</div>;
}