Back

Common JSX Mistakes and How to Avoid Them

Common JSX Mistakes and How to Avoid Them

JSX looks deceptively simple—it’s just HTML in JavaScript, right? Yet even experienced developers stumble over its quirks, especially as React evolves. With React 19’s automatic JSX runtime, Server Components, and the shifting landscape of modern frameworks, these mistakes have new implications. Here’s what still trips developers up and how to avoid these pitfalls.

Key Takeaways

  • Array indices as keys cause reconciliation issues and break React’s concurrent features
  • Server Components require different patterns than client components, especially around browser APIs
  • The automatic JSX runtime changes how your code transforms and requires proper configuration
  • Inline functions and conditional rendering patterns can silently degrade performance

The Evolution of JSX: Why Old Habits Break New Code

The automatic JSX runtime introduced in React 17 eliminated the need to import React in every file, but it also created new confusion. Your JSX now transforms differently—jsx functions replace React.createElement, and misconfigured build tools can silently break your app.

In Server Components, the stakes are higher. JSX that works perfectly client-side crashes when it tries to access window or uses hooks in the wrong context. The rules haven’t just changed; they’ve multiplied.

Critical JSX Pitfalls in Modern React

1. Unstable Keys That Destroy Performance

// ❌ Index as key - causes reconciliation issues
items.map((item, index) => <Item key={index} {...item} />)

// ✅ Stable, unique identifier
items.map(item => <Item key={item.id} {...item} />)

Using array indices as keys remains one of the most damaging JSX mistakes. In React’s concurrent features, unstable keys don’t just cause flickering—they can break Suspense boundaries and trigger unnecessary re-renders across your component tree.

2. Direct Object Rendering

// ❌ Objects aren't valid React children
const user = { name: 'Alice', age: 30 };
return <div>{user}</div>;

// ✅ Render specific properties
return <div>{user.name}</div>;

This error message hasn’t changed since React 15, yet developers still attempt to render objects directly. With TypeScript’s JSX inference, you’ll catch this at compile time—if your tsconfig.json is properly configured.

3. Inline Functions Creating New References

// ❌ New function on every render
<Button onClick={() => handleClick(id)} />

// ✅ Stable reference with useCallback
const handleButtonClick = useCallback(() => handleClick(id), [id]);
<Button onClick={handleButtonClick} />

In React’s rendering pipeline, inline functions don’t just cause performance issues—they break memo optimization and can trigger cascading updates throughout your component tree.

Server Components: Where JSX Rules Change

4. Client-Only Code in Server Components

// ❌ Crashes in Server Components
export default function ServerComponent() {
  const width = window.innerWidth; // ReferenceError
  return <div style={{ width }} />;
}

// ✅ Use client directive or pass from client
'use client';
import { useState, useEffect } from 'react';

export default function ClientComponent() {
  const [width, setWidth] = useState(0);
  useEffect(() => {
    setWidth(window.innerWidth);
  }, []);
  return <div style={{ width }} />;
}

Server Components execute outside the browser where DOM APIs don’t exist. This isn’t a configuration issue—it’s architectural.

5. Async Components Without Suspense

// ❌ Unhandled promise in Server Component
async function UserProfile({ id }) {
  const user = await fetchUser(id);
  return <div>{user.name}</div>;
}

// ✅ Wrap with Suspense boundary
<Suspense fallback={<Loading />}>
  <UserProfile id={userId} />
</Suspense>

React Server Components can be async, but without proper Suspense boundaries, they’ll either block rendering or crash with cryptic errors.

Modern JSX Configuration Pitfalls

6. Mismatched JSX Runtime Configuration

// ❌ Old transform in tsconfig.json
{
  "compilerOptions": {
    "jsx": "react"  // Requires React imports
  }
}

// ✅ Automatic runtime for React 17+
{
  "compilerOptions": {
    "jsx": "react-jsx"  // No React import needed
  }
}

The automatic JSX runtime isn’t just a convenience—it’s required for optimal bundle size and Server Component compatibility. Misconfiguration here causes silent failures that only surface in production.

7. Conditional Rendering Anti-Patterns

// ❌ Returns 0 instead of nothing
{count && <Counter value={count} />}

// ✅ Explicit boolean conversion
{Boolean(count) && <Counter  value={count} />}

When count is 0, JSX renders the number 0, not nothing. This mistake is particularly visible in React Native where text nodes require proper containers.

Prevention Strategies

Configure Your Tools Properly: Set up ESLint with eslint-plugin-react and enable these rules:

  • react/jsx-key
  • react/jsx-no-bind
  • react/display-name

Use TypeScript: With proper JSX configuration, TypeScript catches most of these errors at compile time. Enable strict mode and configure jsx properly in your tsconfig.json.

Understand Your Runtime: Know whether your component runs on the server or client. Next.js 14+ makes this explicit with the 'use client' directive, but the mental model applies everywhere.

Conclusion

JSX mistakes in 2024 aren’t just about syntax—they’re about understanding where and how your code executes. The automatic JSX runtime changed the transformation model. Server Components changed the execution model. React’s concurrent features changed the performance model.

Master these fundamentals, and you’ll write JSX that’s not just correct, but optimized for modern React’s capabilities. The best JSX is invisible—it gets out of the way and lets your components shine.

FAQs

When you use array indices as keys, React can't properly track which items have changed, moved, or been removed. This forces React to re-render more components than necessary and can cause state to be associated with the wrong component after reordering.

While inline functions work functionally, they create new references on every render, breaking React.memo optimizations and potentially causing child components to re-render unnecessarily. For better maintainability, use useCallback for event handlers that depend on props or state.

The react setting uses the classic React.createElement transform requiring React imports in every file. The react-jsx setting uses the automatic runtime introduced in React 17, which handles the transformation without explicit React imports and produces smaller bundles.

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers. Check our GitHub repo and join the thousands of developers in our community.

OpenReplay