React & Next.js Topic
React State Management with Hooks
1. useState - Managing Component State
Basic Usage
jsx
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ name: '', age: 0 });
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<input
value={user.name}
onChange={(e) => setUser({...user, name: e.target.value})}
placeholder="Name"
/>
</div>
);
}
Functional Updates
jsx
function Counter() {
const [count, setCount] = useState(0);
// Use functional update when new state depends on previous state
const increment = () => setCount(prevCount => prevCount + 1);
const incrementBy = (amount) => setCount(prevCount => prevCount + amount);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+1</button>
<button onClick={() => incrementBy(5)}>+5</button>
</div>
);
}
2. useEffect - Handling Side Effects
Basic Effects
jsx
import React, { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
// Effect runs after every render
useEffect(() => {
document.title = user ? `${user.name}'s Profile` : 'Loading...';
});
// Effect runs only when userId changes
useEffect(() => {
if (!userId) return;
const fetchUser = async () => {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const userData = await response.json();
setUser(userData);
} catch (error) {
console.error('Failed to fetch user:', error);
} finally {
setLoading(false);
}
};
fetchUser();
}, [userId]); // Dependency array
// Cleanup effect (runs on unmount)
useEffect(() => {
const handleResize = () => console.log('Window resized');
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // Empty dependency array = runs once on mount
if (loading) return <div>Loading...</div>;
return <div>{user?.name}</div>;
}
3. useMemo - Memoizing Expensive Calculations
jsx
import React, { useState, useMemo } from 'react';
function ExpensiveComponent({ items, filter }) {
// This expensive calculation only re-runs when items or filter change
const filteredItems = useMemo(() => {
console.log('Filtering items...');
return items.filter(item =>
item.name.toLowerCase().includes(filter.toLowerCase())
);
}, [items, filter]); // Recompute only when dependencies change
// Without useMemo, this would run on every render
const sortedItems = useMemo(() => {
return [...filteredItems].sort((a, b) => a.name.localeCompare(b.name));
}, [filteredItems]);
return (
<div>
{sortedItems.map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
);
}
4. useCallback - Memoizing Functions
jsx
import React, { useState, useCallback, memo } from 'react';
const ChildComponent = memo(({ onButtonClick, data }) => {
console.log('Child rendered'); // This won't log unnecessarily
return <button onClick={onButtonClick}>{data}</button>;
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');
// Without useCallback, this function would be recreated on every render
// causing ChildComponent to re-render unnecessarily
const handleClick = useCallback(() => {
setCount(prev => prev + 1);
}, []); // Same function reference unless dependencies change
// Function that depends on state
const handleTextUpdate = useCallback((newText) => {
setText(newText);
console.log(`Count is: ${count}`); // This would use stale closure
}, [count]); // Include count as dependency
return (
<div>
<p>Count: {count}</p>
<input
value={text}
onChange={(e) => setText(e.target.value)}
/>
<ChildComponent
onButtonClick={handleClick}
data={`Count: ${count}`}
/>
</div>
);
}
5. useRef - Persistent References
jsx
import React, { useState, useRef, useEffect } from 'react';
function RefExamples() {
const [count, setCount] = useState(0);
const inputRef = useRef(null);
const previousCountRef = useRef();
const renderCountRef = useRef(0);
// Store previous value
useEffect(() => {
previousCountRef.current = count;
}, [count]);
// Count renders (without causing re-renders)
renderCountRef.current += 1;
const focusInput = () => {
inputRef.current.focus();
inputRef.current.select();
};
return (
<div>
<p>Current: {count}</p>
<p>Previous: {previousCountRef.current}</p>
<p>Renders: {renderCountRef.current}</p>
<input ref={inputRef} type="text" />
<button onClick={focusInput}>Focus Input</button>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
6. Understanding Re-renders
When Components Re-render
jsx
import React, { useState, memo } from 'react';
// Child components re-render when:
// 1. Their own state changes
// 2. Their props change
// 3. Parent component re-renders
const ExpensiveChild = memo(({ user, onUpdate }) => {
console.log('ExpensiveChild rendered');
return (
<div>
<p>{user.name}</p>
<button onClick={() => onUpdate(user.id)}>Update</button>
</div>
);
});
function ParentComponent() {
const [count, setCount] = useState(0);
const [user, setUser] = useState({ id: 1, name: 'John' });
const [text, setText] = useState('');
// This would cause ExpensiveChild to re-render every time
// because a new function is created on each render
const badHandleUpdate = (userId) => {
console.log('Update user:', userId);
};
// This prevents unnecessary re-renders
const goodHandleUpdate = React.useCallback((userId) => {
console.log('Update user:', userId);
}, []);
return (
<div>
<button onClick={() => setCount(count + 1)}>
Re-render Parent ({count})
</button>
<input
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="This triggers re-renders"
/>
{/* With bad function - re-renders every time */}
<ExpensiveChild user={user} onUpdate={badHandleUpdate} />
{/* With good function - only re-renders when user changes */}
<ExpensiveChild user={user} onUpdate={goodHandleUpdate} />
</div>
);
}
Performance Optimization Patterns
jsx
import React, { useState, useMemo, useCallback, memo } from 'react';
// Optimized component
const UserList = memo(({ users, onUserSelect, filter }) => {
const filteredUsers = useMemo(() => {
return users.filter(user =>
user.name.toLowerCase().includes(filter.toLowerCase())
);
}, [users, filter]);
return (
<div>
{filteredUsers.map(user => (
<UserItem
key={user.id}
user={user}
onSelect={onUserSelect}
/>
))}
</div>
);
});
const UserItem = memo(({ user, onSelect }) => {
return (
<div onClick={() => onSelect(user.id)}>
{user.name}
</div>
);
});
function OptimizedParent() {
const [users, setUsers] = useState([]);
const [selectedUserId, setSelectedUserId] = useState(null);
const [filter, setFilter] = useState('');
const handleUserSelect = useCallback((userId) => {
setSelectedUserId(userId);
}, []);
const addUser = useCallback((name) => {
setUsers(prev => [...prev, { id: Date.now(), name }]);
}, []);
return (
<div>
<input
value={filter}
onChange={(e) => setFilter(e.target.value)}
placeholder="Filter users..."
/>
<UserList
users={users}
filter={filter}
onUserSelect={handleUserSelect}
/>
</div>
);
}
Key Principles for State Management
1. State Structure
jsx
// Avoid: Separate related state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [age, setAge] = useState(0);
// Better: Group related state
const [user, setUser] = useState({
firstName: '',
lastName: '',
age: 0
});
// Update nested state properly
setUser(prev => ({
...prev,
firstName: 'John'
}));
2. Effect Dependencies
jsx
function Component({ id, fetchData }) {
const [data, setData] = useState(null);
// Missing dependency
useEffect(() => {
fetchData(id).then(setData);
}, [id]); // Missing fetchData
// Correct dependencies
useEffect(() => {
fetchData(id).then(setData);
}, [id, fetchData]); // Include all dependencies
// Using useCallback for functions
const stableFetchData = useCallback((id) => {
// fetch implementation
}, []); // No dependencies needed if function doesn't use props/state
}
3. When to Use Each Hook
-
useState: Component-level state
-
useEffect: Side effects, subscriptions, API calls
-
useMemo: Expensive calculations, derived data
-
useCallback: Function props to optimized children
-
useRef: DOM access, persistent values without re-renders
This comprehensive approach to React hooks will help you build efficient, performant applications with proper state management and minimal unnecessary re-renders.
Comments (0)
No comments yet. Be the first to comment!