React Patterns Every Developer Should Know: Scale and Optimize React Applications
Essential Patterns for Scalable and Optimized Applications

Search for a command to run...
Essential Patterns for Scalable and Optimized Applications

No comments yet. Be the first to comment.
Large language models can answer questions, write content, summarize information, and help with many other tasks. However, they have one important limitation: they do not automatically know everything

Uploading images directly from an Expo app to Amazon S3 can make your uploads faster and reduce work on your backend server. However, you should never put AWS access keys inside your Expo app. Anyone

Today, you do not need to pay for an AI API just to test AI models. You can run many open-source or open-weight AI models directly on your own laptop or PC. This means the model runs on your machine,

Deploying for Indian user data safety

Why I spent 3 years manually excluding IDE files from every single repo (and how one config line fixed everything)

React development has significantly evolved, leading to essential patterns for writing clean, maintainable, and performant code. This guide covers critical React patterns, from basic state management to advanced component architecture, based on practical developer experience. Whether you're new to React or refining your skills, mastering these patterns will enhance your code quality and development efficiency.
Before diving into specific patterns, it's crucial to understand how React components work under the hood. React components follow a predictable lifecycle that consists of mounting, updating, and unmounting phases, with hooks providing a way to tap into this lifecycle from functional components.

React Hook Flow Diagram illustrating the component lifecycle.
The React Hook flow demonstrates how different hooks interact during the component lifecycle. Understanding this flow is essential for implementing the patterns effectively, as it helps developers predict when their code will execute and how state updates will propagate through the application.

React Hooks Lifecycle diagram illustrating component mounting and updating.
The first pattern I've found most impactful involves keeping UI components as thin wrappers over data, avoiding the overuse of local state unless absolutely necessary. This pattern emphasizes that UI state should be independent of business logic, leading to more maintainable and testable code.
// ❌ Anti-pattern: Mixing business logic with UI state
function UserDashboard() {
const [user, setUser] = useState(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
setIsLoading(true);
fetchUser().then(setUser).finally(() => setIsLoading(false));
}, []);
return (
<div>
{isLoading && <Spinner />}
{user && <UserProfile user={user} />}
</div>
);
}
// ✅ Better approach: Separate business logic from UI state
function UserDashboard() {
const { user, isLoading } = useUserData();
const [isProfileExpanded, setIsProfileExpanded] = useState(false);
return (
<div>
{isLoading && <Spinner />}
{user && (
<UserProfile
user={user}
isExpanded={isProfileExpanded}
onToggleExpanded={setIsProfileExpanded}
/>
)}
</div>
);
}
// Custom hook handles all business logic
function useUserData() {
const [state, setState] = useState({ user: null, isLoading: false });
useEffect(() => {
setState(prev => ({ ...prev, isLoading: true }));
fetchUser()
.then(user => setState({ user, isLoading: false }))
.catch(() => setState({ user: null, isLoading: false }));
}, []);
return state;
}
This approach provides several benefits including improved testability, better separation of concerns, and enhanced reusability. By extracting business logic into custom hooks, components become more focused on their primary responsibility: rendering UI.
The derived state pattern emphasizes calculating values during render instead of storing them in state unnecessarily. This approach reduces complexity and prevents synchronization issues between related state values.
// ❌ Anti-pattern: Storing derived values in state
function ShoppingCart({ items }) {
const [cartItems, setCartItems] = useState(items);
const [total, setTotal] = useState(0);
useEffect(() => {
const newTotal = cartItems.reduce((sum, item) => sum + item.price, 0);
setTotal(newTotal);
}, [cartItems]);
return <div>Total: ${total}</div>;
}
// ✅ Better approach: Calculate derived values during render
function ShoppingCart({ items }) {
const [cartItems, setCartItems] = useState(items);
// Derived values calculated during render
const total = cartItems.reduce((sum, item) => sum + item.price, 0);
const itemCount = cartItems.length;
return (
<div>
<h2>Cart ({itemCount} items)</h2>
<div>Total: ${total}</div>
</div>
);
}
For expensive calculations, you can optimize using useMemo:
function ExpensiveCalculationComponent({ data, filter }) {
const processedData = useMemo(() => {
return data
.filter(item => item.name.includes(filter))
.sort((a, b) => a.priority - b.priority);
}, [data, filter]);
return (
<div>
{processedData.map(item => (
<ItemDisplay key={item.id} item={item} />
))}
</div>
);
}
Instead of managing related state with multiple useState hooks, using a state machine approach makes code easier to reason about and prevents impossible states.

// ❌ Anti-pattern: Multiple useState for related state
function FormSubmission() {
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
const [error, setError] = useState(null);
const handleSubmit = async (formData) => {
setIsLoading(true);
setError(null);
try {
await submitForm(formData);
setIsSuccess(true);
} catch (err) {
setError(err.message);
} finally {
setIsLoading(false);
}
};
return (
<div>
<button disabled={isLoading}>
{isLoading ? 'Submitting...' : 'Submit'}
</button>
{isSuccess && <div>Success!</div>}
{error && <div>Error: {error}</div>}
</div>
);
}
// ✅ Better approach: Single state machine
function FormSubmission() {
const [state, setState] = useState({
status: 'idle', // 'idle' | 'loading' | 'success' | 'error'
error: null
});
const handleSubmit = async (formData) => {
setState({ status: 'loading', error: null });
try {
await submitForm(formData);
setState({ status: 'success', error: null });
} catch (error) {
setState({ status: 'error', error: error.message });
}
};
return (
<div>
<button disabled={state.status === 'loading'}>
{state.status === 'loading' ? 'Submitting...' : 'Submit'}
</button>
{state.status === 'success' && <div>Success!</div>}
{state.status === 'error' && <div>Error: {state.error}</div>}
</div>
);
}
When components have nested conditional logic or complex rendering requirements, creating new component abstractions improves readability and maintainability.

// ❌ Anti-pattern: Nested conditional logic in single component
function UserProfile({ user, currentUser }) {
return (
<div>
{user ? (
<div>
<h1>{user.name}</h1>
{user.id === currentUser.id ? (
<div>
<button>Edit Profile</button>
{user.isPremium ? (
<div>Premium Badge</div>
) : (
<button>Upgrade</button>
)}
</div>
) : (
<div>
<button>{user.isFollowing ? 'Unfollow' : 'Follow'}</button>
</div>
)}
</div>
) : (
<div>User Not Found</div>
)}
</div>
);
}
// ✅ Better approach: Extract components for different concerns
function UserProfile({ user, currentUser }) {
if (!user) return <UserNotFound />;
const isOwner = user.id === currentUser.id;
return (
<div>
<UserHeader user={user} />
{isOwner ? <OwnerActions user={user} /> : <VisitorActions user={user} />}
</div>
);
}
function OwnerActions({ user }) {
return (
<div>
<button>Edit Profile</button>
{user.isPremium ? <PremiumBadge /> : <button>Upgrade</button>}
</div>
);
}
function VisitorActions({ user }) {
return (
<div>
<FollowButton user={user} />
</div>
);
}
This pattern transforms a single, complex component into multiple focused components, each with a single responsibility. The benefits include improved readability, easier testing, better reusability, and simplified debugging.
Rather than hiding logic in useEffect dependencies, explicitly define logic to make code more predictable and easier to debug.

// ❌ Anti-pattern: Hidden logic in useEffect dependencies
function UserSearch({ query, filters }) {
const [results, setResults] = useState([]);
useEffect(() => {
if (query) {
searchUsers(query, filters).then(setResults);
}
}, [query, filters]); // What triggers this?
return (
<div>
{results.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
// ✅ Better approach: Explicit logic
function UserSearch({ query, filters }) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const performSearch = useCallback(async (searchQuery, searchFilters) => {
if (!searchQuery.trim()) {
setResults([]);
return;
}
setIsLoading(true);
try {
const searchResults = await searchUsers(searchQuery, searchFilters);
setResults(searchResults);
} catch (error) {
console.error('Search failed:', error);
setResults([]);
} finally {
setIsLoading(false);
}
}, []);
useEffect(() => {
performSearch(query, filters);
}, [query, filters, performSearch]);
return (
<div>
{isLoading && <div>Searching...</div>}
{results.map(user => <UserCard key={user.id} user={user} />)}
</div>
);
}
The setTimeout function should be used sparingly in React applications, and when necessary, it should be well-documented and properly cleaned up.
// ❌ Anti-pattern: Unexplained setTimeout usage
function NotificationComponent({ onClose }) {
useEffect(() => {
setTimeout(() => {
onClose();
}, 3000);
}, [onClose]);
return <div>Notification</div>;
}
// ✅ Better approach: Documented and cleaned up setTimeout
function NotificationComponent({ onClose, autoCloseDelay = 3000 }) {
useEffect(() => {
// Auto-close notification after specified duration for better UX
const timeoutId = setTimeout(() => {
onClose();
}, autoCloseDelay);
// Cleanup: Clear timeout if component unmounts
return () => clearTimeout(timeoutId);
}, [onClose, autoCloseDelay]);
return (
<div>
Notification
<button onClick={onClose}>×</button>
</div>
);
}
Better alternatives to setTimeout for common use cases:
// For debouncing input
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
function SearchInput({ onSearch }) {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
useEffect(() => {
if (debouncedQuery) onSearch(debouncedQuery);
}, [debouncedQuery, onSearch]);
return (
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
);
}
When implementing these patterns, consider performance implications and optimization strategies. React's rendering behavior and the component lifecycle should guide your implementation decisions.
// Optimized pattern implementation with performance considerations
function OptimizedUserList({ users, filters }) {
// Memoize expensive filtering operations
const processedUsers = useMemo(() => {
return users.filter(user => {
return Object.entries(filters).every(([key, value]) => {
if (!value) return true;
return user[key]?.toLowerCase().includes(value.toLowerCase());
});
});
}, [users, filters]);
const handleUserClick = useCallback((userId) => {
console.log('User clicked:', userId);
}, []);
return (
<div>
{processedUsers.map(user => (
<MemoizedUserCard key={user.id} user={user} onClick={handleUserClick} />
))}
</div>
);
}
const MemoizedUserCard = memo(function UserCard({ user, onClick }) {
return (
<div onClick={() => onClick(user.id)}>
<h3>{user.name}</h3>
<p>{user.email}</p>
</div>
);
});
Understanding common mistakes helps prevent bugs and maintain code quality:
// ❌ Too many state variables
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [email, setEmail] = useState('');
// ✅ Group related state
const [userForm, setUserForm] = useState({
firstName: '',
lastName: '',
email: ''
});
// ❌ Memory leak potential
useEffect(() => {
const interval = setInterval(fetchUpdates, 1000);
}, []);
// ✅ Proper cleanup
useEffect(() => {
const interval = setInterval(fetchUpdates, 1000);
return () => clearInterval(interval);
}, []);
// ❌ Object created on every render
function Component() {
const config = { theme: 'dark', size: 'large' }; // New object every render
return <ChildComponent config={config} />;
}
// ✅ Stable reference
function Component() {
const config = useMemo(() => ({ theme: 'dark', size: 'large' }), []);
return <ChildComponent config={config} />;
}
Implementing these React patterns effectively requires understanding both the technical aspects and the underlying principles:
Separation of Concerns - Keep business logic separate from UI concerns
Predictable State Management - Use state machines and explicit logic flows
Component Composition - Break complex components into focused, reusable pieces
Performance Awareness - Consider rendering implications and optimize when necessary
Code Clarity - Write code that explicitly communicates intent and behavior
These patterns represent proven solutions to common React development challenges. By mastering them progressively and applying them consistently, you can create more maintainable, performant, and scalable React applications. Remember that patterns are tools to solve problems, not rules to follow blindly - always consider the specific context and requirements of your application when deciding which patterns to implement.
The journey to mastering React patterns is iterative and requires practice with real-world applications. Start with the foundational patterns, build confidence through implementation, and gradually incorporate more advanced techniques as your understanding deepens.