← Back to list

Reactjs JSX Anti-Patterns You Must Avoid in 2025

Inline functions, deeply nested components, unnecessary fragments — and the hidden costs they bring to performance, readability, and Core…

Suresh Kumar Ariya Gowder · 2025-08-23 10:44 · 0 claps · 7.0 min read paywalled
#jsx-anti-patterns #react-jsx-best-practices #unnecessary-re-renders #react-clean-code #jsx-readability
Open on Medium ↗
Wiki topics: RAG · RAG & Retrieval 🌐 · Web Development

Reactjs JSX Anti-Patterns You Must Avoid in 2025

Inline functions, deeply nested components, unnecessary fragments — and the hidden costs they bring to performance, readability, and Core Web Vitals.

Introduction

JSX was one of React’s most brilliant innovations. Instead of separating logic (JavaScript) and structure (HTML), React combined them into one declarative syntax. JSX makes components more intuitive and encourages a functional approach to UI development.

But like any powerful tool, JSX can be misused. And here’s the kicker: even senior developers, under pressure to ship, slip into anti-patterns that hurt performance, readability, and scalability.

In 2025, with React powering Next.js 15 apps, SaaS dashboards, streaming UIs, and mission-critical e-commerce sites, clean JSX is not optional. Every unnecessary render, every bloated DOM tree, every missed accessibility attribute has real costs:

  • Performance: Core Web Vitals (LCP, INP, CLS) directly impact SEO and conversion.
  • Maintainability: Developers waste time deciphering nested, noisy JSX.
  • Scalability: A small anti-pattern in one component multiplies into technical debt across the entire app.

This article goes deep into 8+ JSX anti-patterns that you must avoid in 2025, how they creep into codebases, and how to fix them with best practices that scale for teams and users alike.

1. Inline Functions Inside JSX

The Anti-Pattern

// ❌ Anti-pattern: inline function re-created each render
<button onClick={() => doSomething(id)}>Click</button>

Why it happens:

  • Inline functions are quick to write and visually close to where they’re used.
  • In small demos, they appear harmless.

Why it’s bad:

  • React creates a new function reference on every render.
  • If passed down as a prop, it breaks React.memo and forces unnecessary re-renders.
  • On interactive UIs (tables, lists, dashboards), it adds up, degrading INP (Interaction to Next Paint).

The Fix

// ✅ Stable handler with useCallback
const handleClick = useCallback(() => doSomething(id), [id]);

<button onClick={handleClick}>Click</button>

Industry lesson: In one SaaS dashboard with 500+ buttons, refactoring inline handlers reduced wasted renders by ~28%, shaving ~300ms off INP.

Pro Tip: For performance-critical lists, memoize both handlers (useCallback) and children (React.memo) for maximum stability.

2. Deeply Nested JSX Trees

The Anti-Pattern

// ❌ JSX nesting hell
<div>
  <header>
    <nav>
      <ul>
        <li>
          <a href="/about">
            <span>
              <strong>About</strong>
            </span>
          </a>
        </li>
      </ul>
    </nav>
  </header>
</div>

Why it happens:

  • Developers often translate designs 1:1 into JSX without abstraction.
  • Fear of “over-abstracting” leads to long, unreadable trees.

Why it’s bad:

  • Hurts readability: difficult to debug or scan.
  • Increases cognitive load during code review.
  • Performance-wise, any parent re-render propagates through the entire nested tree.

The Fix: Component Extraction

// ✅ Break into composable components
function NavItem({ href, label }: { href: string; label: string }) {
  return (
    <li>
      <a href={href}>{label}</a>
    </li>
  );
}

function Navbar() {
  return (
    <nav>
      <ul>
        <NavItem href="/about" label="About" />
        <NavItem href="/contact" label="Contact" />
      </ul>
    </nav>
  );
}

Impact: Smaller components:

  • Improve testability.
  • Encourage reuse.
  • Let React skip re-renders with React.memo.

Rule of thumb: If a JSX block is longer than 15 lines, consider extracting it into a component.

3. Unnecessary Fragments

The Anti-Pattern

// ❌ Wrapping for no reason
<>
  <div>Hello</div>
  <div>World</div>
</>

Why it happens:

  • Developers use <>...</> reflexively whenever returning multiple elements.

Why it’s bad:

  • Adds noise to code with no functional benefit.
  • Makes JSX harder to scan for real fragments.

The Fix

// ✅ No unnecessary wrapper
<div>Hello</div>
<div>World</div>

When to use fragments:

  • To group multiple children without adding extra DOM nodes.
  • Example: returning multiple <td>s inside a table row.

Misusing fragments leads to DOM bloat — hurting memory and potentially CLS (layout shift) if wrappers affect structure.

4. Conditional Rendering Without Abstraction

The Anti-Pattern

// ❌ Ternary soup inside JSX
<div>
  {isLoggedIn 
    ? (user.isAdmin ? <AdminPanel /> : <UserDashboard />)
    : <LoginForm />}
</div>

Why it happens:

  • JSX encourages inline logic, but complex conditions spiral quickly.

Why it’s bad:

  • Creates “logic soup” inside markup.
  • Makes debugging extremely painful.

The Fix

// ✅ Extract logic outside JSX
let content;
if (!isLoggedIn) content = <LoginForm />;
else if (user.isAdmin) content = <AdminPanel />;
else content = <UserDashboard />;

return <div>{content}</div>;

Best practice:

  • Move conditional logic before the return.
  • For repeated patterns, extract into utility functions.

Benefit: Readability + easier unit testing (test getContentForUser() instead of parsing JSX).

5. Overusing Inline Styles

The Anti-Pattern

// ❌ Recreates object every render
<div style={{ marginTop: "10px", color: "blue" }}>Hello</div>

Why it happens:

  • Inline styles feel convenient, especially for quick tweaks.

Why it’s bad:

  • New object each render = breaks React.memo.
  • Blocks browser CSS optimizations like caching.
  • Harder to maintain a design system.

The Fix

// ✅ Tailwind example
<div className="mt-2 text-blue-600">Hello</div>
// ✅ CSS Modules
<div className={styles.title}>Hello</div>

Performance impact: Inline styles often bypass CSSOM optimizations → can negatively affect CLS and render speed.

6. Business Logic Buried in JSX

The Anti-Pattern

// ❌ Mixing logic + rendering
<ul>
  {items
    .filter(item => item.inStock && item.price < 50)
    .map(item => (
      <li>{item.name}</li>
    ))}
</ul>

Why it’s bad:

  • Violates Separation of Concerns.
  • Hard to test logic independently.
  • In larger UIs, makes JSX unreadable.

The Fix

// ✅ Extract logic outside JSX
const filteredItems = items.filter(item => item.inStock && item.price < 50);

<ul>
  {filteredItems.map(item => (
    <li key={item.id}>{item.name}</li>
  ))}
</ul>

Industry example: In an e-commerce app, moving filtering/sorting logic into useMemo reduced wasted renders and improved filter INP latency by 20%.

7. Wrapper <div> Overuse

The Anti-Pattern

// ❌ DOM soup
<div className="outer">
  <div className="inner">
    <div className="content">Hello</div>
  </div>
</div>

Why it happens:

  • Developers add <div> wrappers to satisfy JSX’s single-parent rule.
  • Lack of semantic awareness.

Why it’s bad:

  • DOM bloat: bigger HTML payloads.
  • Hurts accessibility (screen readers).
  • Adds unnecessary CSS complexity.

The Fix

// ✅ Semantic, minimal markup
<section>
  <p>Hello</p>
</section>

Best practice: Use <section>, <article>, <header>, <main>, <footer>, and <span> appropriately. Your SEO and a11y scores will thank you.

8. Ignoring Accessibility in JSX

The Anti-Pattern

// ❌ No accessible label
<button>?</button>

Why it’s bad:

  • Screen readers can’t interpret functionality.
  • Fails WCAG compliance.
  • Reduces SEO for interactive elements.

The Fix

// ✅ a11y-friendly
<button aria-label="Help">?</button>

Performance connection: Accessible apps load more predictably, reducing CLS from dynamic content and improving UX for everyone.

9. Large Lists Without Virtualization

The Anti-Pattern

// ❌ Rendering 5000 items directly
<ul>
  {bigList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>

Why it’s bad:

  • Blocks main thread with massive DOM updates.
  • Users see sluggish scrolling → poor INP.

The Fix: Virtualization

import { FixedSizeList as List } from "react-window";

<List height={400} width={600} itemSize={35} itemCount={bigList.length}>
  {({ index, style }) => <li style={style}>{bigList[index].name}</li>}
</List>

Industry standard: Always virtualize lists over 100 items.

Case Study: Refactoring a JSX-Heavy Fintech Dashboard

I once worked with a fintech client whose React/Next.js dashboard had grown organically over three years. Multiple teams had contributed, each under deadlines, and JSX anti-patterns crept in everywhere. On the surface, the app worked — but performance audits and developer experience told a different story.

The Problems We Found

  1. Inline Functions → 30% Wasted Renders
  • Buttons, dropdowns, and table rows all used inline onClick={() => ...} and onChange={() => ...} handlers.
  • Every render created new function identities, breaking memoization (React.memo, useMemo).
  • Profiling showed components like <TransactionRow> were re-rendering even when props didn’t change.

Impact: The dashboard felt sluggish when filtering or scrolling — clicks took ~400ms to respond, hurting INP (Interaction to Next Paint).

2. Deeply Nested JSX → Debugging Pain

  • Some files had 500+ lines of JSX with 6–7 levels of nested <div>, <span>, <section> wrappers.
  • A single <TransactionList> component handled filtering, mapping, rendering, and UI logic inline.
  • When a bug appeared (e.g., wrong label on certain rows), developers had to trace through deeply nested ternaries and map chains.

Impact: Debugging and onboarding took twice as long — new devs needed 2–3 weeks before they could confidently push changes.

3. DOM Bloat from <div> Wrappers → 12,000+ Nodes

  • Overuse of wrapper <div>s (“just to make it work”) led to huge DOM trees.
  • Pages with transaction tables easily exceeded 12,000 DOM nodes.
  • Chrome DevTools showed layout thrashing and poor CLS (Cumulative Layout Shift) when filtering or resizing.

Impact: The dashboard failed Core Web Vitals audits. CLS spikes made the UI feel unstable, especially on lower-end laptops.

Refactor Strategy

We didn’t rewrite from scratch — we targeted anti-patterns step by step.

  1. Memoized Handlers
  • Replaced inline callbacks with useCallback.
  • Wrapped rows and buttons in React.memo.
  • Result: only rows that changed re-rendered.
const handleSelect = useCallback(
  (id: string) => toggleSelection(id),
  [toggleSelection]
);

<TransactionRow onSelect={handleSelect} />

2. Extracted Components

  • Split massive files into small, single-responsibility components:
  • <TransactionRow> → rendering only a row.
  • <TransactionTable> → iterating over rows.
  • <Filters> → handling filter UI separately.
  • Each file shrank from ~500 lines → ~100 lines.

Developers could now unit test rows, filters, and tables independently.

3. Virtualized Lists

  • Replaced <table> mapping over 5000 transactions with react-window.
  • Only rendered rows visible in the viewport.
  • Reduced rendering load from 5000 DOM nodes → ~50 at a time.
import { FixedSizeList } from "react-window";

<FixedSizeList
  height={600}
  itemSize={50}
  itemCount={transactions.length}
>
  {({ index, style }) => (
    <TransactionRow style={style} data={transactions[index]} />
  )}
</FixedSizeList>

4. Semantic HTML for Structure

  • Removed unnecessary <div class="outer"> wrappers.
  • Replaced with semantic <section>, <header>, <article>.
  • Cleaned up CSS by leveraging semantic selectors instead of wrapper chains.

The DOM node count dropped from 12,000 → ~4,500 on heavy pages.

The Results

After three months of targeted refactoring:

Performance Gains:

  • INP (Interaction to Next Paint) improved by 42% — buttons and filters responded in ~200ms instead of ~400ms.
  • Virtualized lists eliminated freezes when loading large datasets.

Technical Wins:

  • DOM nodes reduced by 60%, making layout calculations smoother and CLS stable.
  • Chrome Lighthouse scores jumped from 62 → 89 on performance audits.

Team Productivity:

  • New developers onboarded in 1.5 weeks instead of 3.
  • Smaller, modular components made code reviews 40% faster.
  • Bugs in transaction rows were fixed in hours instead of days.

Conclusion

JSX is powerful but deceptively simple. Writing JSX that scales requires discipline:

  1. Avoid inline functions — use stable callbacks.
  2. Break deeply nested trees into smaller components.
  3. Don’t wrap in fragments or <div>s unnecessarily.
  4. Keep business logic out of render.
  5. Use semantic, accessible markup.
  6. Virtualize large lists for performance.

Mindset shift for 2025: JSX is not “HTML in JS.” It’s a UI language that demands clean architecture, performance awareness, and accessibility-first thinking.

Master these, and your React/Next.js apps won’t just run — they’ll scale beautifully, keeping Core Web Vitals green, developers happy, and users delighted.

Please share your thoughts in comment.

To buy “To buy “HTML5 + Tailwind CSS 4: Responsive Layout Component Library”. Please click here

To Buy “20 Responsive CSS Layout Templates for Modern UI Development”, Please click here

To Buy “Next.js 15 & React 19 Cookbook — The Definitive Guide to Modern Web Apps”, Please click here


메타데이터
post_id
f58e44d08abf
slug
reactjs-jsx-anti-patterns-you-must-avoid-in-2025-f58e44d08abf
url
https://medium.com/@sureshdotariya/reactjs-jsx-anti-patterns-you-must-avoid-in-2025-f58e44d08abf
canonical_url
https://medium.com/@sureshdotariya/reactjs-jsx-anti-patterns-you-must-avoid-in-2025-f58e44d08abf
author_url
https://medium.com/@sureshdotariya
status
ok
fetched_at
2026-07-22 06:57:32