
5 Common React Mistakes Developers Make (And How to Fix Them)
// ❌ Bad: No key at all {items.map((item) => ( <div>{item.name}</div> ))} </pre><h3>Why It's a Problem</h3><p>Using indices as keys can cause serious issues when:</p><ul><li>Items are reordered</li><li>Items are added or removed</li><li>The list is filtered or sorted</li></ul><p>React uses keys to identify which items have changed, been added, or been removed. When you use indices, React can't properly track these changes, leading to:</p><ul><li>Incorrect component state</li><li>Performance issues</li><li>Unexpected UI behavior</li></ul><h3>The Solution</h3><p>Always use unique, stable identifiers as keys:</p><pre class="ql-syntax" spellcheck="false">// ✅ Good: Using unique ID {items.map((item) => ( <div key={item.id}>{item.name}</div> ))}
// ✅ Good: If no ID exists, create one {items.map((item) => ( <div key={item.id || generateId()}>{item.name}</div> ))} </pre><h4>Best Practices</h4><ul><li>Use database IDs when available</li><li>Generate unique IDs using libraries like uuid if needed</li><li>Never use Math.random() as it changes on every render</li><li>Only use index as a last resort for static lists that never change</li></ul><h2>2. Mutating State Directly</h2><h3>The Problem</h3><p>Directly modifying state is one of the most dangerous mistakes in React:</p><pre class="ql-syntax" spellcheck="false">// ❌ Bad: Mutating state directly const [user, setUser] = useState({ name: 'John', age: 25 }); const updateAge = () => { user.age = 26; // This won't trigger a re-render! setUser(user); };
// ❌ Bad: Mutating array state const [items, setItems] = useState([1, 2, 3]); const addItem = () => { items.push(4); // This won't work correctly! setItems(items); }; </pre><h3>Why It's a Problem</h3><p>React relies on immutability to detect changes. When you mutate state directly:</p><ul><li>React doesn't detect the change</li><li>Components don't re-render</li><li>UI becomes out of sync with state</li><li>Debugging becomes extremely difficult</li></ul><h3>The Solution</h3><p>Always create new objects or arrays when updating state:</p><pre class="ql-syntax" spellcheck="false">// ✅ Good: Creating new object const [user, setUser] = useState({ name: 'John', age: 25 }); const updateAge = () => { setUser({ ...user, age: 26 }); // Or using callback form setUser(prev => ({ ...prev, age: 26 })); };
// ✅ Good: Creating new array const [items, setItems] = useState([1, 2, 3]); const addItem = () => { setItems([...items, 4]); // Or using callback form setItems(prev => [...prev, 4]); };
// ✅ Good: Removing item const removeItem = (id) => { setItems(items.filter(item => item.id !== id)); };
// ✅ Good: Updating nested object const [user, setUser] = useState({ name: 'John', address: { city: 'New York', zip: '10001' } }); const updateCity = (newCity) => { setUser({ ...user, address: { ...user.address, city: newCity } }); }; </pre><h4>Pro Tips</h4><ul><li>Use the spread operator (...) for shallow copies</li><li>Use structuredClone() or libraries like Immer for deep copies</li><li>Always use the callback form when new state depends on previous state</li></ul><h2>3. Overusing useEffect</h2><h3>The Problem</h3><p>Many developers use useEffect for everything, even when it's not necessary:</p><pre class="ql-syntax" spellcheck="false">// ❌ Bad: Unnecessary useEffect const [count, setCount] = useState(0); const [doubled, setDoubled] = useState(0); useEffect(() => { setDoubled(count * 2); }, [count]);
// ❌ Bad: useEffect for derived state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(${firstName} ${lastName});
}, [firstName, lastName]);
</pre><h3>Why It's a Problem</h3><p>Overusing useEffect leads to:</p><ul><li>Unnecessary re-renders</li><li>Complex dependency arrays</li><li>Difficult-to-debug code</li><li>Performance issues</li><li>Race conditions</li></ul><h3>The Solution</h3><p>Calculate derived values during render:</p><pre class="ql-syntax" spellcheck="false">// ✅ Good: Calculate during render
const [count, setCount] = useState(0);
const doubled = count * 2; // No useEffect needed!
// ✅ Good: Derived state
const [firstName, setFirstName] = useState('');
const [lastName, setLastName] = useState('');
const fullName = ${firstName} ${lastName}; // Simple!
// ✅ Good: Use useMemo for expensive calculations const [items, setItems] = useState([]); const sortedItems = useMemo(() => { return [...items].sort((a, b) => a.value - b.value); }, [items]); </pre><h3>When to Actually Use useEffect</h3><p>Use useEffect only for:</p><ul><li>Fetching data from APIs</li><li>Setting up subscriptions</li><li>Manually manipulating the DOM</li><li>Integrating with third-party libraries</li><li>Synchronizing with external systems</li></ul><pre class="ql-syntax" spellcheck="false">// ✅ Good: Fetching data useEffect(() => { const fetchData = async () => { const response = await fetch('/api/data'); const data = await response.json(); setData(data); }; fetchData(); }, []);
// ✅ Good: Subscription useEffect(() => { const subscription = eventEmitter.subscribe(handleEvent); return () => { subscription.unsubscribe(); }; }, []); </pre><h2>4. Not Memoizing Expensive Computations</h2><h3>The Problem</h3><p>Performing expensive calculations on every render:</p><pre class="ql-syntax" spellcheck="false">// ❌ Bad: Expensive calculation on every render function ProductList({ products }) { const sortedProducts = products .filter(p => p.inStock) .sort((a, b) => b.rating - a.rating) .slice(0, 10); return ( <div> {sortedProducts.map(product => ( <div key={product.id}>{product.name}</div> ))} </div> ); } </pre><h3>Why It's a Problem</h3><ul><li>Slows down your application</li><li>Causes unnecessary re-renders</li><li>Poor user experience</li><li>Wasted CPU cycles</li></ul><h3>The Solution</h3><p>Use useMemo for expensive computations and useCallback for functions:</p><pre class="ql-syntax" spellcheck="false">// ✅ Good: Memoized calculation function ProductList({ products }) { const sortedProducts = useMemo(() => { return products .filter(p => p.inStock) .sort((a, b) => b.rating - a.rating) .slice(0, 10); }, [products]); return ( <div> {sortedProducts.map(product => ( <div key={product.id}>{product.name}</div> ))} </div> ); }
// ✅ Good: Memoized callback function SearchBar({ onSearch }) { const [query, setQuery] = useState(''); const handleSearch = useCallback(() => { onSearch(query); }, [query, onSearch]); return ( <input value={query} onChange={(e) => setQuery(e.target.value)} onKeyPress={(e) => e.key === 'Enter' && handleSearch()} /> ); } </pre><h4>When to Use Memoization</h4><ul><li>Complex calculations or transformations</li><li>Filtering/sorting large arrays</li><li>Creating objects or arrays that are passed as props</li><li>Callback functions passed to child components</li></ul><h4>When NOT to Use Memoization</h4><ul><li>Simple calculations (addition, string concatenation)</li><li>Primitive values</li><li>Small arrays (< 100 items)</li><li>Components that rarely re-render</li></ul><h2>5. Prop Drilling Instead of Context</h2><h3>The Problem</h3><p>Passing props through multiple levels of components:</p><pre class="ql-syntax" spellcheck="false">// ❌ Bad: Prop drilling function App() { const [user, setUser] = useState(null); return ( <Dashboard user={user} setUser={setUser} /> ); }
function Dashboard({ user, setUser }) { return ( <Sidebar user={user} setUser={setUser} /> ); }
function Sidebar({ user, setUser }) { return ( <UserProfile user={user} setUser={setUser} /> ); }
function UserProfile({ user, setUser }) { return <div>{user.name}</div>; } </pre><h3>Why It's a Problem</h3><ul><li>Makes code hard to maintain</li><li>Intermediate components receive props they don't use</li><li>Difficult to refactor</li><li>Tight coupling between components</li></ul><h3>The Solution</h3><p>Use React Context for global or shared state:</p><pre class="ql-syntax" spellcheck="false">// ✅ Good: Using Context import { createContext, useContext, useState } from 'react';
const UserContext = createContext();
export function UserProvider({ children }) { const [user, setUser] = useState(null); return ( <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> ); }
export function useUser() { const context = useContext(UserContext); if (!context) { throw new Error('useUser must be used within UserProvider'); } return context; }
// Usage function App() { return ( <UserProvider> <Dashboard /> </UserProvider> ); }
function Dashboard() { return <Sidebar />; }
function Sidebar() { return <UserProfile />; }
function UserProfile() { const { user, setUser } = useUser(); return <div>{user?.name}</div>; } </pre><h4>Best Practices for Context</h4><ol><li><strong>Split contexts by concern</strong></li><li><strong>Memoize context values</strong></li><li><strong>Use context for truly global state</strong> (theme, auth, localization, feature flags)</li><li><strong>Don't use context for everything</strong> (local state should stay local, consider prop drilling for 1-2 levels, use state management libraries for complex state)</li></ol><h2>Bonus Tips for Better React Code</h2><h3>1. Use TypeScript</h3><pre class="ql-syntax" spellcheck="false">interface User { id: string; name: string; email: string; }
function UserCard({ user }: { user: User }) { return <div>{user.name}</div>; } </pre><h3>2. Extract Custom Hooks</h3><pre class="ql-syntax" spellcheck="false">// Custom hook for API calls function useApi(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false)); }, [url]); return { data, loading, error }; } </pre><h3>3. Use Error Boundaries</h3><pre class="ql-syntax" spellcheck="false">class ErrorBoundary extends React.Component { state = { hasError: false }; static getDerivedStateFromError(error) { return { hasError: true }; } render() { if (this.state.hasError) { return <div>Something went wrong.</div>; } return this.props.children; } } </pre><h2>Conclusion</h2><p>Avoiding these common React mistakes will help you write cleaner, more maintainable, and performant code. Remember:</p><ol><li><strong>Use proper keys</strong> in lists</li><li><strong>Never mutate state</strong> directly</li><li><strong>Don't overuse useEffect</strong> - calculate during render when possible</li><li><strong>Memoize expensive operations</strong></li><li><strong>Use Context with useMemo and useCallback</strong></li></ol>