Ten Hidden React Component Pitfalls That Slow Down Apps in the React 19 Era
Ten common React component mistakes, from missing semantic HTML to skipped memoization, why React 19 and the React Compiler don't hide them, with fixes.
This article is published in English.
Sources: 10 React Component Mistakes That Are Secretly Breaking Your App in 2026
React components look simple until small missteps pile up into accessibility gaps, slow rendering and bugs that are hard to spot. React 19, the React Compiler and Server Components have changed how apps get built, and that makes these slips more expensive, not less. Here are ten component-level mistakes that quietly hurt an app, with the fix for each one.
1. Using <div> Everywhere Instead of Semantic HTML
The Mistake: Reaching for a generic <div> for every UI element. Navigation becomes a <div>. A button turns into <div onClick={...}>. An article is wrapped in a <div>. Even a form label gets the same treatment.
// ❌ Bad: Div soup
function Navbar() {
return (
<div className="navbar">
<div className="logo" onClick={() => router.push('/')}>Logo</div>
<div className="nav-links">
<div className="nav-item" onClick={() => setPage('home')}>Home</div>
<div className="nav-item" onClick={() => setPage('about')}>About</div>
</div>
</div>
);
}
Why It Breaks Your App:
- Accessibility breakdown: assistive technology can't recognize navigation landmarks, interactive controls, or headings. Someone using a screen reader hears something like "clickable, text: Home" rather than "navigation, link, Home."
- Broken keyboard access:
<div>tags aren't focusable out of the box, so anyone navigating with a keyboard can't tab into your nav menu. - Weaker SEO: search engines depend on semantic markup such as
<nav>,<main>, and<article>to parse the structure of a page. A page built entirely from<div>s reads as low-quality content to crawlers. - Server Components in React 19: because SSR and streaming mean the initial HTML payload is what both users and bots see before hydration kicks in, semantic structure matters even more.
The Fix:
// ✅ Good: Semantic HTML
function Navbar() {
return (
<nav className="navbar" aria-label="Main navigation">
<a href="/" className="logo">Logo</a>
<ul className="nav-links">
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
);
}
2026 Best Practice: Take advantage of the new use API in React 19 alongside accessibility metadata, and run your components through auditing tools like axe-core or the WAVE browser extension. Use <button> for anything clickable, <nav> for navigation blocks, and <section> paired with aria-labelledby when marking off a page region.
2. Inline Styles in JSX (Killing Performance)
The Mistake: Defining style objects inline within JSX, which generates a brand-new object reference on every single render.
// ❌ Bad: New object every render
function Badge({ color, children }) {
return (
<span style={{ backgroundColor: color, padding: '4px 8px', borderRadius: '4px' }}>
{children}
</span>
);
}
Why It Breaks Your App:
- Extra reconciliation work: since a fresh style object is created each render, React's diffing logic detects a "changed" prop and touches the DOM node even though nothing actually changed.
- Slower CSS-in-JS: libraries that parse inline style declarations pay this cost repeatedly, making the problem worse.
- Compiler blind spots: the React 19 compiler is capable of memoizing components, but it has no way to stabilize inline object references on its own. Once it spots a newly created object, it skips optimizing that part.
- Missed caching opportunities: browsers cannot cache inline style declarations the way they cache stylesheets. A CSS class gets parsed once, while inline styles are re-parsed on every mount.
The Fix:
// ✅ Good: CSS classes with dynamic data attributes
function Badge({ color, children }) {
return (
<span
className="badge"
data-color={color}
style={{ '--badge-color': color }}
>
{children}
</span>
);
}
// CSS
// .badge { background-color: var(--badge-color); padding: 4px 8px; border-radius: 4px; }
Alternatively, Tailwind CSS utilities work well when used carefully with arbitrary values:
// Acceptable: Tailwind utility classes
function Badge({ color, children }) {
return (
<span className="px-2 py-1 rounded" style={{ backgroundColor: color }}>
{children}
</span>
);
}
2026 Best Practice: Push static styling into CSS modules, Tailwind classes, or a zero-runtime CSS-in-JS approach. Save inline styles for values that are genuinely dynamic, like the width of a progress bar, and even then lean on CSS custom properties where possible.
3. Not Using the key Prop Correctly in Lists
The Mistake: Relying on the array index as a key, skipping the key prop altogether, or generating a random key on each render.
// ❌ Bad: Index as key
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
</ul>
);
}
// ❌ Worse: Random key
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={Math.random()}>{todo.text}</li>
))}
</ul>
);
}
Why It Breaks Your App:
- Index-based keys: when list items get reordered, React matches the same key to the same position and ends up reusing the wrong DOM node for the wrong item. This mixes up input state, focus, and internal component state across items.
- Randomly generated keys: since a new key is produced on every render, React tears down and rebuilds every DOM node each time an update happens. This wrecks performance and wipes out any form state in the list.
- React 19 transitions: with concurrent rendering and transitions in play, faulty keys can cause visible tearing in the UI and corrupted state whenever an update gets interrupted.
The Fix:
// ✅ Good: Stable, unique identifier
function TodoList({ todos }) {
return (
<ul>
{todos.map((todo) => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
);
}
If there's no natural unique identifier available, create one when the data is first produced rather than at render time:
// At data creation, not in the component
const todos = items.map(item => ({
...item,
id: crypto.randomUUID(), // or your backend's ID
}));
2026 Best Practice: When a list comes from a Server Component, make sure the server itself supplies stable identifiers. Avoid deriving keys from array position unless the list is genuinely static and only ever grows by appending new items.
4. Mutating Props Directly
The Mistake: Treating props as if they were mutable local variables and changing them inside a child component.
// ❌ Bad: Mutating props directly
function SortButton({ items }) {
const handleSort = () => {
items.sort((a, b) => a.price - b.price); // MUTATES the prop!
onSort(items);
};
return <button onClick={handleSort}>Sort by Price</button>;
}
Why It Breaks Your App:
- Silent corruption of the parent: The parent's state gets modified without ever knowing it happened. React's change-detection relies on reference comparison, so the mutation goes completely unnoticed.
- Stale re-render bugs: Since
itemsis still pointing at the same array in memory, the parent never re-renders, leaving the interface showing outdated data. - React Compiler confusion: The compiler assumes props flow immutably through your tree. Mutating them directly invalidates its automatic memoization logic.
- Loss of predictability: Any other component that references the same array will suddenly see the mutated, sorted version without warning.
The Fix:
// ✅ Good: Immutable update
function SortButton({ items, onSort }) {
const handleSort = () => {
const sorted = [...items].sort((a, b) => a.price - b.price);
onSort(sorted);
};
return <button onClick={handleSort}>Sort by Price</button>;
}
2026 Best Practice: Reach for Immer when nested mutations feel unavoidable, and lock in an ESLint rule such as prefer-readonly-props if you're on TypeScript. Assume every prop is readonly unless proven otherwise.
5. Too Many Props (Instead of Composition)
The Mistake: Building sprawling "god components" that accept a dozen or more props just to control every possible internal variation.
// ❌ Bad: Prop explosion
function Card({
title,
subtitle,
imageUrl,
imageAlt,
showImage,
imagePosition,
actions,
showActions,
actionAlignment,
badge,
badgeColor,
onBadgeClick,
footer,
showFooter,
footerContent,
borderRadius,
shadow,
padding,
variant,
}) {
return (
<div className={`card card--${variant}`} style={{ borderRadius, padding, boxShadow: shadow }}>
{showImage && (
<div className={`card__image card__image--${imagePosition}`}>
<img src={imageUrl} alt={imageAlt} />
</div>
)}
<div className="card__content">
<h2>{title}</h2>
{subtitle && <p>{subtitle}</p>}
</div>
{/* ... more conditional rendering ... */}
</div>
);
}
Why It Breaks Your App:
- A brittle API surface: Every new requirement means adding another prop, touching the component's internals, and risking something else breaking.
- Bloated bundles: The component ends up importing and rendering code paths for features that a given usage never actually needs.
- Painful test coverage: You're forced to validate an ever-growing matrix of prop combinations.
- Friction with React Server Components: Overloaded prop lists tend to push more logic onto the client than is actually necessary.
The Fix:
// ✅ Good: Composition
function Card({ children, className }) {
return (
<div className={clsx('card', className)}>
{children}
</div>
);
}
function CardImage({ src, alt, position = 'top' }) {
return (
<div className={`card__image card__image--${position}`}>
<img src={src} alt={alt} loading="lazy" />
</div>
);
}
function CardContent({ title, subtitle }) {
return (
<div className="card__content">
<h2>{title}</h2>
{subtitle && <p>{subtitle}</p>}
</div>
);
}
function CardActions({ children, align = 'end' }) {
return (
<div className={`card__actions card__actions--${align}`}>
{children}
</div>
);
}
// Usage
<Card>
<CardImage src="/photo.jpg" alt="Product" />
<CardContent title="Headphones" subtitle="Noise cancelling" />
<CardActions>
<button>Add to Cart</button>
</CardActions>
</Card>
2026 Best Practice: Lean on the Compound Component pattern or slot-based composition instead. Let the consumer of the component decide on structure and content, the component itself should just supply the container.
6. Not Using React.Fragment (Unnecessary Divs)
The Mistake: Wrapping sibling elements inside a plain <div> purely to satisfy JSX's rule that a component must return a single root node.
// ❌ Bad: Wrapper div for layout that breaks semantics
function TableCell({ label, value }) {
return (
<div> {/* This div breaks the <table> structure! */}
<td>{label}</td>
<td>{value}</td>
</div>
);
}// ❌ Bad: Extra div in a list
function ListItems({ items }) {
return (
<ul>
{items.map(item => (
<div key={item.id}> {/* Invalid HTML: div inside ul */}
<li>{item.name}</li>
<li>{item.description}</li>
</div>
))}
</ul>
);
}
Why It Breaks Your App:
- Invalid markup: A
<div>isn't a legal child of elements like<table>,<ul>,<select>, or<dl>. Browsers will silently rewrite the DOM to fix this, which leads to hydration mismatches during server-side rendering. - Layout interference: Extra wrapper elements disrupt CSS Grid, Flexbox behavior, and the
gapproperty. - Hydration failures in React: Given how common streaming SSR and Server Components are now, this kind of invalid nesting can trigger fatal hydration mismatches that crash the app on the client.
The Fix:
// ✅ Good: React.Fragment
function TableCell({ label, value }) {
return (
<>
<td>{label}</td>
<td>{value}</td>
</>
);
}
// ✅ Good: Fragment with key (required in lists)
function ListItems({ items }) {
return (
<ul>
{items.map(item => (
<React.Fragment key={item.id}>
<li>{item.name}</li>
<li>{item.description}</li>
</React.Fragment>
))}
</ul>
);
}
2026 Best Practice: Reach for the <> </> shorthand when you don't need a key, and fall back to <React.Fragment key={...}> when rendering arrays of fragments that require one. Periodically inspect your component tree in React DevTools to catch and remove unnecessary wrapper divs.
7. Rendering Falsy Numbers with && (Hidden Bugs)
The Mistake: Relying on && to conditionally render something, without accounting for cases where the left operand evaluates to 0, NaN, or an empty string.
// ❌ Bad: Shows "0" on screen when items.length is 0
function Cart({ items }) {
return (
<div>
<h1>Shopping Cart</h1>
{items.length && (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
</div>
);
}
// ❌ Bad: accountBalance could be 0
function AccountSummary({ account }) {
return (
<div>
{account.balance && <p>Balance: ${account.balance}</p>}
</div>
);
}
Why It Breaks Your App:
- React actually renders
0: writing{0 && <Component />}doesn't produce nothing, it prints the literal number0as text. - Invisible UI defects: users end up staring at a stray "0" somewhere on the page, which looks broken and can throw off your layout.
- Real-world zero values: things like account balances, item counts, or scores are frequently zero in a valid, expected way. Treating
0as falsy with&&means you either render nothing when you shouldn't, or render the digit itself when you don't want to.
The Fix:
// ✅ Good: Explicit boolean check
function Cart({ items }) {
return (
<div>
<h1>Shopping Cart</h1>
{items.length > 0 && (
<ul>
{items.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
)}
</div>
);
}
// ✅ Good: Nullish coalescing for defaults, ternary for conditionals
function AccountSummary({ account }) {
return (
<div>
{account.balance != null ? (
<p>Balance: ${account.balance}</p>
) : (
<p>Loading balance...</p>
)}
</div>
);
}
2026 Best Practice: Avoid && for conditional rendering altogether. Prefer a ternary (condition ? : null) or force an explicit boolean with something like Boolean(count) && .... Add an ESLint rule to your project that flags risky && patterns in JSX.
8. Skipping Memoization on Costly JSX
The Mistake: Letting an expensive subtree re-render on every parent update even though the relevant props haven't actually changed.
// ❌ Bad: HeavyChart re-renders every time Parent re-renders
function Dashboard({ data, filter }) {
const [hoveredItem, setHoveredItem] = useState(null);
return (
<div>
<Sidebar onHover={setHoveredItem} />
<HeavyChart data={data} filter={filter} /> {/* Re-renders on every hover! */}
<Tooltip item={hoveredItem} />
</div>
);
}
function HeavyChart({ data, filter }) {
// Expensive SVG rendering, D3 calculations, etc.
return <svg>{/* thousands of nodes */}</svg>;
}
Why It Breaks Your App:
- Wasted computation:
HeavyChartreruns its rendering logic even whendataandfilterare unchanged. - Frame drops: if rendering
HeavyCharttakes something like 50ms, a simple hover event on a sidebar can tank the frame rate down to around 20fps. - Limits of the React Compiler: automatic memoization from the compiler works within a component, but it can't optimize across component boundaries if the parent keeps handing down freshly created callback references.
The Fix:
// ✅ Good: Memoize the expensive component
const HeavyChart = memo(function HeavyChart({ data, filter }) {
// Expensive rendering
return <svg>{/* thousands of nodes */}</svg>;
});
function Dashboard({ data, filter }) {
const [hoveredItem, setHoveredItem] = useState(null);
// Stabilize the callback if passed to children
const handleHover = useCallback((item) => setHoveredItem(item), []);
return (
<div>
<Sidebar onHover={handleHover} />
<HeavyChart data={data} filter={filter} />
<Tooltip item={hoveredItem} />
</div>
);
}
2026 Best Practice: React 19's compiler reduces the need for manual memoization in straightforward cases, but for genuinely heavy subtrees, wrapping with memo is still worthwhile. Use the React DevTools Profiler to confirm where the real bottlenecks are before reaching for memoization.
9. Blending Presentation and Business Logic
The Mistake: Building one component that simultaneously fetches data, transforms it, tracks form state, runs side effects, and renders the markup.
// ❌ Bad: God component
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [isEditing, setIsEditing] = useState(false);
const [formData, setFormData] = useState({});
useEffect(() => {
fetchUser(userId).then(data => {
setUser(data);
setFormData(data);
setLoading(false);
});
}, [userId]);
const handleSubmit = async () => {
await updateUser(userId, formData);
setIsEditing(false);
};
if (loading) return <Spinner />;
return (
<div>
<h1>{user.name}</h1>
{isEditing ? (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={e => setFormData({...formData, name: e.target.value})}
/>
</form>
) : (
<button onClick={() => setIsEditing(true)}>Edit</button>
)}
</div>
);
}
Why It Breaks Your App:
- Hard to test: you can't verify the fetching logic without mounting the whole component, and you can't test the rendered UI without faking the network layer.
- Not reusable: the form behavior is locked inside this one component and can't be shared elsewhere.
- Difficult to reason about: state, side effects, and markup are tangled together, so a UI tweak can accidentally break the data-handling logic.
- Poor fit with Server Components: by 2026 conventions, data fetching belongs in Server Components, while Client Components should be reserved for interactivity.
The Fix:
// ✅ Good: Separated concerns
// Container / Logic Hook
function useUserProfile(userId) {
const { data: user, isLoading } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId),
});
const updateMutation = useMutation({
mutationFn: updateUser,
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['user', userId] }),
});
return { user, isLoading, updateUser: updateMutation.mutate };
}
// UI Component
function UserProfile({ userId }) {
const { user, isLoading, updateUser } = useUserProfile(userId);
const [isEditing, setIsEditing] = useState(false);
if (isLoading) return <Spinner />;
return (
<div>
<h1>{user.name}</h1>
{isEditing ? (
<UserProfileForm
initialData={user}
onSubmit={(data) => {
updateUser({ userId, ...data });
setIsEditing(false);
}}
onCancel={() => setIsEditing(false)}
/>
) : (
<button onClick={() => setIsEditing(true)}>Edit</button>
)}
</div>
);
}
// Reusable Form Component
function UserProfileForm({ initialData, onSubmit, onCancel }) {
const [formData, setFormData] = useState(initialData);
return (
<form onSubmit={(e) => { e.preventDefault(); onSubmit(formData); }}>
<input
value={formData.name}
onChange={e => setFormData(prev => ({...prev, name: e.target.value}))}
/>
<button type="submit">Save</button>
<button type="button" onClick={onCancel}>Cancel</button>
</form>
);
}
Recommended Approach for 2026: Split responsibilities into custom hooks for logic, plain presentational components for markup, and Server Components for data loading. A component should either perform an action or display something, and it's rare for one to justify doing both well.
10. Not Using TypeScript Interfaces for Props
The Mistake: Depending on prop-types, scattered JSDoc comments, or, in the worst case, skipping type checking for props entirely.
// ❌ Bad: No types
function Button(props) {
return <button className={props.variant}>{props.children}</button>;
}
// ❌ Bad: PropTypes at runtime only
import PropTypes from 'prop-types';
function Button(props) {
return <button className={props.variant}>{props.children}</button>;
}
Button.propTypes = {
variant: PropTypes.string,
children: PropTypes.node,
};
Why It Breaks Your App:
- Silent failures: typos in prop names, missing required values, and mismatched types surface as runtime crashes instead of being caught before deployment.
- Refactoring becomes painful: renaming a single prop forces you to manually search the whole codebase, whereas a type system immediately flags every call site that needs updating.
- Weaker developer experience: without types, you lose autocomplete, inline documentation hints, and the safety net for automated renames.
- Poor alignment with React 19: newer React capabilities such as Server Components, using
usewith Promises, and forwarding refs are built with TypeScript's type model in mind, so skipping types means losing compile-time guarantees these features rely on.
The Fix:
// ✅ Good: Explicit TypeScript interface
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
type?: 'button' | 'submit' | 'reset';
}
function Button({
variant = 'primary',
size = 'md',
disabled = false,
onClick,
children,
type = 'button'
}: ButtonProps) {
return (
<button
type={type}
className={`btn btn--${variant} btn--${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
}
Recommended Approach for 2026: Enable strict mode (strict: true in tsconfig.json). Model props with interface declarations rather than type aliases, since interfaces tend to produce clearer error messages and are easier to extend. Reach for React.ComponentProps when you need to derive types from native HTML elements while building wrapper components. Avoid falling back on any for prop types under any circumstances.
Summary: The 2026 Component Checklist
Before you merge a component, run through this checklist:
- [ ] Did you choose semantic HTML instead of stacking generic
<div>elements? - [ ] Are styles defined in CSS rather than inline style objects?
- [ ] Do list items rely on stable, unique
keyvalues? - [ ] Are props always treated as read-only?
- [ ] Can the component be composed, rather than needing a dozen-plus configuration props?
- [ ] Did you replace unnecessary wrapper divs with
Fragment? - [ ] Are conditional renders written as ternaries or explicit boolean checks instead of
&&? - [ ] Is costly JSX memoized where it matters?
- [ ] Is business logic separated from markup, living in hooks or utility functions?
- [ ] Are all props fully described using TypeScript interfaces?
None of these mistakes will crash your app on day one. Instead, they erode it gradually, through mounting accessibility debt, creeping performance loss, and subtle state corruption. Addressing them now means your React codebase will hold up through the next framework upgrade, the next big refactor, and whichever team eventually takes over maintaining what you built.