React Hooks revolutionized how we write React components, allowing us to use state and other React features without writing classes. In this comprehensive guide, we’ll dive deep into five essential hooks: useState, useEffect, useCallback, useRef, and useMemo. Understanding these hooks will help you write cleaner, more efficient React applications.
What are React Hooks?
Hooks are functions that let you “hook into” React state and lifecycle features from function components. Introduced in React 16.8, they’ve become the standard way to write React components.
“Hooks solve exactly known problems we’ve had building apps with React.” — React Documentation
useState: Managing Component State
The useState hook is the foundation of state management in functional components. It allows you to add state to your components with minimal boilerplate.
Basic Syntax
import { useState } from 'react';
const [state, setState] = useState(initialValue);
Practical Example
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('');
const [todos, setTodos] = useState([]);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Enter your name"
/>
<button onClick={() => setTodos([...todos, `Task ${todos.length + 1}`])}>
Add Todo
</button>
</div>
);
}
Best Practices
1. Use functional updates for state that depends on previous state:
// Not recommended
setCount(count + 1);
// Recommended
setCount(prevCount => prevCount + 1);
2. Split state when values change independently:
// Avoid combining unrelated state
const [user, setUser] = useState({ name: '', age: 0, email: '' });
// Better: separate state for independent values
const [name, setName] = useState('');
const [age, setAge] = useState(0);
const [email, setEmail] = useState('');
3. Initialize state carefully:
// Expensive computation runs on every render
const [value, setValue] = useState(expensiveComputation());
// Lazy initialization - computation runs only once
const [value, setValue] = useState(() => expensiveComputation());
useEffect: Handling Side Effects
The useEffect hook lets you perform side effects in function components. It’s your go-to hook for data fetching, subscriptions, DOM manipulation, and more.
Basic Syntax
import { useEffect } from 'react';
useEffect(() => {
// Side effect logic
return () => {
// Cleanup (optional)
};
}, [dependencies]);
Common Use Cases
1. Data Fetching
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUser = async () => {
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUser(data);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]);
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
2. Event Listeners
function WindowTracker() {
const [windowSize, setWindowSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight
});
};
window.addEventListener('resize', handleResize);
// Cleanup on unmount
return () => window.removeEventListener('resize', handleResize);
}, []);
return <div>Window: {windowSize.width} x {windowSize.height}</div>;
}
3. Subscriptions
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => connection.disconnect();
}, [roomId]);
return <ChatWindow />;
}
Understanding Dependency Arrays
// Runs on every render
useEffect(() => {
console.log('No dependencies');
});
// Runs only once (on mount)
useEffect(() => {
console.log('Empty dependency array');
}, []);
// Runs when specific values change
useEffect(() => {
console.log('Dependency changed:', count);
}, [count]);
useCallback: Optimizing Function References
The useCallback hook returns a memoized version of a callback function. It’s essential for preventing unnecessary re-renders of child components that rely on reference equality.
Basic Syntax
import { useCallback } from 'react';
const memoizedCallback = useCallback(() => {
doSomething(a, b);
}, [a, b]);
When to Use useCallback
1. Passing callbacks to optimized child components:
function Parent() {
const [count, setCount] = useState(0);
const [other, setOther] = useState(0);
// Without useCallback, this function is recreated on every render
const handleClick = useCallback(() => {
console.log('Count:', count);
}, [count]);
return (
<>
<MemoizedButton onClick={handleClick} />
<button onClick={() => setOther(other + 1)}>Other: {other}</button>
</>
);
}
2. Dependencies in useEffect:
function SearchComponent({ searchQuery }) {
const handleSearch = useCallback((query) => {
console.log('Searching:', query);
}, []);
useEffect(() => {
handleSearch(searchQuery);
}, [handleSearch, searchQuery]);
return <SearchInput onSearch={handleSearch} />;
}
Common Mistake: Overusing useCallback
// Don't wrap everything in useCallback
const handleClick = useCallback(() => {
doSomething();
}, []);
// Only use when needed for optimization
const handleClick = () => {
doSomething();
};
useRef: Accessing DOM and Persisting Values
The useRef hook serves two main purposes: accessing DOM elements directly and storing mutable values that persist across renders without causing re-renders.
Basic Syntax
import { useRef } from 'react';
const refContainer = useRef(initialValue);
Use Cases
1. Accessing DOM Elements
function FocusInput() {
const inputRef = useRef(null);
const focusInput = () => {
inputRef.current?.focus();
};
return (
<>
<input ref={inputRef} type="text" placeholder="Click button to focus" />
<button onClick={focusInput}>Focus Input</button>
</>
);
}
2. Storing Mutable Values
function Timer() {
const [count, setCount] = useState(0);
const intervalRef = useRef(null);
useEffect(() => {
intervalRef.current = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(intervalRef.current);
}, []);
const stopTimer = () => {
clearInterval(intervalRef.current);
};
return (
<div>
<p>Timer: {count}s</p>
<button onClick={stopTimer}>Stop</button>
</div>
);
}
3. Tracking Previous Values
function Counter() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count;
}, [count]);
const prevCount = prevCountRef.current;
return (
<div>
<p>Current: {count}, Previous: {prevCount}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
useMemo: Expensive Computation Optimization
The useMemo hook memoizes the result of a computation, recalculating only when dependencies change. It’s crucial for optimizing performance in computationally expensive operations.
Basic Syntax
import { useMemo } from 'react';
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
When to Use useMemo
1. Expensive Calculations
function Fibonacci({ n }) {
const fib = useMemo(() => {
const calculateFib = (num) => {
if (num <= 1) return num;
return calculateFib(num - 1) + calculateFib(num - 2);
};
return calculateFib(n);
}, [n]);
return <div>Fibonacci({n}) = {fib}</div>;
}
2. Filtering and Sorting Large Lists
function ProductList({ products, filter, sortBy }) {
const filteredAndSortedProducts = useMemo(() => {
let result = [...products];
if (filter) {
result = result.filter(p => p.category === filter);
}
if (sortBy) {
result.sort((a, b) => a[sortBy] - b[sortBy]);
}
return result;
}, [products, filter, sortBy]);
return (
<ul>
{filteredAndSortedProducts.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
}
3. Object Reference Stability
function UserSettings({ userId }) {
const defaultSettings = useMemo(() => ({
theme: 'dark',
notifications: true,
language: 'en'
}), []);
const [settings, setSettings] = useState(defaultSettings);
// defaultSettings won't change on re-renders
return <SettingsForm settings={settings} />;
}
Combining Hooks: A Complete Example
Here’s a practical example that combines multiple hooks:
function Dashboard({ userId }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [filter, setFilter] = useState('all');
const searchInputRef = useRef(null);
const renderCount = useRef(0);
// Track render count (debugging)
renderCount.current += 1;
// Fetch data when userId changes
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}/data`);
const result = await response.json();
setData(result);
} catch (error) {
console.error('Fetch failed:', error);
} finally {
setLoading(false);
}
};
fetchData();
}, [userId]);
// Memoize filtered data
const filteredData = useMemo(() => {
if (!data) return [];
return data.items.filter(item => {
if (filter === 'all') return true;
return item.category === filter;
});
}, [data, filter]);
// Memoize filter handler
const handleFilterChange = useCallback((newFilter) => {
setFilter(newFilter);
}, []);
// Focus search input
const focusSearch = () => {
searchInputRef.current?.focus();
};
if (loading) return <div>Loading...</div>;
return (
<div>
<input ref={searchInputRef} placeholder="Search..." />
<button onClick={focusSearch}>Focus Search</button>
<FilterSelector value={filter} onChange={handleFilterChange} />
<DataList items={filteredData} />
<small>Render count: {renderCount.current}</small>
</div>
);
}
Hook Comparison Table
| Hook | Purpose | Re-renders Component? | Use When |
|---|---|---|---|
| useState | State management | Yes | You need reactive state |
| useEffect | Side effects | No (but can trigger state updates) | Data fetching, subscriptions, DOM manipulation |
| useCallback | Memoize functions | No | Passing stable callbacks to optimized children |
| useRef | DOM access / mutable values | No | Direct DOM access, storing mutable values |
| useMemo | Memoize values | No | Expensive computations, stable object references |
Performance Tips
-
Don’t optimize prematurely - Only use
useMemoanduseCallbackwhen you have a performance issue or need reference stability. -
Keep dependency arrays accurate - Missing dependencies can cause bugs; extra dependencies can hurt performance.
-
Use the ESLint plugin - Install
eslint-plugin-react-hooksto catch common mistakes. -
Profile before optimizing - Use React DevTools Profiler to identify actual performance bottlenecks.
Conclusion
Mastering these five hooks—useState, useEffect, useCallback, useRef, and useMemo—gives you the foundation to build modern, efficient React applications. Remember:
- useState for component state
- useEffect for side effects
- useCallback for stable function references
- useRef for DOM access and mutable values
- useMemo for expensive computations
Start with the basics, understand when each hook is appropriate, and optimize only when necessary. Happy coding!
Member discussion
0 commentsStart the conversation
Become a member of >hacksubset_ to start commenting.
Already a member? Sign in