Steven's Knowledge
Troubleshooting

Rendering

Rendering performance issues, hydration errors, and UI update problems in frontend applications

Rendering Troubleshooting

Rendering issues directly impact user experience — from blank screens to sluggish interactions. This guide addresses common rendering problems across React and modern frontend frameworks.


1. Hydration Mismatch Errors (SSR/SSG)

Problem

Warning: Text content did not match. Server: "March 5, 2025" Client: "March 6, 2025"
Error: Hydration failed because the initial UI does not match what was rendered on the server.
  • Content differs between server-rendered HTML and client-rendered output
  • Layout flickers on initial load
  • Interactive elements don't respond after page load

Root Cause

Server and client render different output due to:

  • Date/time differences (server timezone vs client timezone)
  • Math.random() or Date.now() producing different values
  • Browser-only APIs (window, localStorage) accessed during render
  • Conditional rendering based on client-only state

Solution

Use useEffect for client-only values:

function Timestamp() {
  const [time, setTime] = useState<string | null>(null);

  useEffect(() => {
    setTime(new Date().toLocaleString());
  }, []);

  if (!time) return <span>Loading...</span>; // Matches server output
  return <span>{time}</span>;
}

suppressHydrationWarning for intentional mismatches:

<time dateTime={date.toISOString()} suppressHydrationWarning>
  {date.toLocaleDateString()}
</time>

Next.js dynamic with ssr: false for client-only components:

import dynamic from 'next/dynamic';

const ClientOnlyChart = dynamic(() => import('./Chart'), {
  ssr: false,
  loading: () => <ChartSkeleton />,
});

2. Infinite Re-render Loops

Problem

Error: Too many re-renders. React limits the number of renders to prevent an infinite loop.
function BadComponent() {
  const [count, setCount] = useState(0);

  // ✗ Called during render — triggers re-render — infinite loop
  setCount(count + 1);

  // ✗ Object literal creates new reference every render — infinite loop
  useEffect(() => {
    fetchData();
  }, [{ page: 1 }]); // New object on every render

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

Root Cause

  • State updates called directly in render body (not in event handler or effect)
  • useEffect dependencies that change every render (objects, arrays, functions)
  • Derived state that triggers its own update

Solution

Move state updates to effects or event handlers:

function GoodComponent() {
  const [count, setCount] = useState(0);

  // ✓ State update in effect, runs only once
  useEffect(() => {
    setCount(prev => prev + 1);
  }, []);

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

Memoize dependencies:

function SearchResults({ filters }: { filters: Filters }) {
  // ✓ Memoize object to maintain referential equality
  const stableFilters = useMemo(() => filters, [filters.query, filters.category]);

  useEffect(() => {
    fetchResults(stableFilters);
  }, [stableFilters]);
}

Derive state instead of syncing it:

// ✗ Bad — syncing state causes extra renders
function FilteredList({ items, query }: Props) {
  const [filtered, setFiltered] = useState(items);

  useEffect(() => {
    setFiltered(items.filter(item => item.name.includes(query)));
  }, [items, query]);
}

// ✓ Good — derive during render
function FilteredList({ items, query }: Props) {
  const filtered = useMemo(
    () => items.filter(item => item.name.includes(query)),
    [items, query]
  );

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

3. Slow List Rendering — Large Dataset Performance

Problem

Rendering thousands of items causes:

  • Sluggish scrolling
  • High memory usage
  • Long initial render time
  • Unresponsive UI during updates

Solution

Virtual scrolling with TanStack Virtual:

import { useVirtualizer } from '@tanstack/react-virtual';

function VirtualList({ items }: { items: Item[] }) {
  const parentRef = useRef<HTMLDivElement>(null);

  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 50, // Estimated row height
    overscan: 5,            // Render 5 extra items outside viewport
  });

  return (
    <div ref={parentRef} style={{ height: '400px', overflow: 'auto' }}>
      <div style={{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }}>
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <ListItem item={items[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

Pagination for server-side data:

function PaginatedList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['items'],
    queryFn: ({ pageParam = 0 }) => fetchItems({ offset: pageParam, limit: 20 }),
    getNextPageParam: (lastPage) => lastPage.nextOffset,
  });

  return (
    <div>
      {data?.pages.flatMap(page => page.items).map(item => (
        <ListItem key={item.id} item={item} />
      ))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          Load More
        </button>
      )}
    </div>
  );
}

4. Unnecessary Component Re-renders

Problem

Components re-render even when their output hasn't changed, causing performance degradation.

Diagnosis

// React DevTools Profiler or manual logging
function ExpensiveComponent({ data }: Props) {
  console.log('ExpensiveComponent rendered'); // Check how often this fires

  // Or use React DevTools "Highlight updates when components render"
}

Solution

React.memo for expensive components:

const ExpensiveList = React.memo(function ExpensiveList({ items }: { items: Item[] }) {
  return (
    <ul>
      {items.map(item => (
        <li key={item.id}>{/* Complex rendering */}</li>
      ))}
    </ul>
  );
});

useCallback for stable function references:

function Parent() {
  const [count, setCount] = useState(0);

  // ✓ Stable reference — won't cause child re-render
  const handleClick = useCallback((id: string) => {
    console.log('clicked', id);
  }, []);

  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onClick={handleClick} />
    </>
  );
}

React Compiler (React 19+) — automatic memoization:

// With React Compiler, manual memo/useCallback/useMemo are unnecessary
// The compiler automatically optimizes re-renders
function Parent() {
  const [count, setCount] = useState(0);

  const handleClick = (id: string) => {
    console.log('clicked', id);
  };

  // React Compiler automatically memoizes this
  return (
    <>
      <button onClick={() => setCount(c => c + 1)}>Count: {count}</button>
      <ExpensiveChild onClick={handleClick} />
    </>
  );
}

5. Memory Leaks — Components Not Cleaning Up

Problem

  • Memory usage grows over time
  • Browser tab becomes sluggish
  • DevTools shows increasing number of detached DOM nodes

Root Cause

  • Event listeners not removed on unmount
  • Timers (setInterval, setTimeout) not cleared
  • WebSocket/EventSource connections not closed
  • Subscriptions not unsubscribed
  • Async operations updating unmounted components

Solution

Clean up all side effects:

function LiveData({ channelId }: { channelId: string }) {
  const [data, setData] = useState<Data | null>(null);

  useEffect(() => {
    const ws = new WebSocket(`wss://api.example.com/${channelId}`);

    ws.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };

    const interval = setInterval(() => {
      ws.send(JSON.stringify({ type: 'ping' }));
    }, 30000);

    const handleResize = () => { /* ... */ };
    window.addEventListener('resize', handleResize);

    // ✓ Clean up everything
    return () => {
      ws.close();
      clearInterval(interval);
      window.removeEventListener('resize', handleResize);
    };
  }, [channelId]);

  return <div>{/* render data */}</div>;
}

AbortController for async operations:

useEffect(() => {
  const controller = new AbortController();

  async function loadData() {
    try {
      const response = await fetch('/api/data', { signal: controller.signal });
      const result = await response.json();
      setData(result); // Safe — aborted fetch throws, won't reach here
    } catch (err) {
      if (err instanceof DOMException && err.name === 'AbortError') return;
      setError(err);
    }
  }

  loadData();
  return () => controller.abort();
}, []);

6. White Screen of Death (WSOD)

Problem

Application renders a blank white screen with no visible error. Common in production where error overlay is not available.

Root Cause

  • Unhandled JavaScript error crashes the React tree
  • Missing Error Boundaries
  • Failed dynamic imports
  • Invalid JSON parsing
  • Accessing properties on null/undefined

Solution

Error Boundaries at multiple levels:

import { Component, type ErrorInfo, type ReactNode } from 'react';

class ErrorBoundary extends Component<
  { children: ReactNode; fallback?: ReactNode },
  { hasError: boolean; error?: Error }
> {
  state = { hasError: false, error: undefined as Error | undefined };

  static getDerivedStateFromError(error: Error) {
    return { hasError: true, error };
  }

  componentDidCatch(error: Error, info: ErrorInfo) {
    // Report to error tracking service
    reportError({ error, componentStack: info.componentStack });
  }

  render() {
    if (this.state.hasError) {
      return this.props.fallback ?? (
        <div role="alert">
          <h2>Something went wrong</h2>
          <button onClick={() => this.setState({ hasError: false })}>
            Try again
          </button>
        </div>
      );
    }
    return this.props.children;
  }
}

// Use at multiple levels
function App() {
  return (
    <ErrorBoundary fallback={<FullPageError />}>
      <Header />
      <ErrorBoundary fallback={<SectionError />}>
        <MainContent />
      </ErrorBoundary>
      <ErrorBoundary fallback={<SectionError />}>
        <Sidebar />
      </ErrorBoundary>
    </ErrorBoundary>
  );
}

Retry for dynamic imports:

function lazyWithRetry(importFn: () => Promise<any>, retries = 3) {
  return lazy(async () => {
    for (let i = 0; i < retries; i++) {
      try {
        return await importFn();
      } catch (err) {
        if (i === retries - 1) throw err;
        await new Promise(r => setTimeout(r, 1000 * (i + 1)));
      }
    }
    throw new Error('Failed to load module');
  });
}

const Dashboard = lazyWithRetry(() => import('./Dashboard'));

7. React Strict Mode Double Rendering

Problem

In development, components render twice, effects run twice:

// Console shows:
// "Component rendered" (×2)
// "Effect fired" (×2)

Root Cause

React 18 Strict Mode intentionally double-invokes renders and effects in development to surface impure code and missing cleanup.

Solution

This is expected behavior in development only. The fix is to ensure your code handles it correctly:

// ✗ Problem: effect without cleanup
useEffect(() => {
  const connection = createConnection();
  connection.connect(); // Connects twice, never disconnects first
}, []);

// ✓ Solution: proper cleanup
useEffect(() => {
  const connection = createConnection();
  connection.connect();
  return () => connection.disconnect(); // Cleanup runs between double-invoke
}, []);

Do NOT remove Strict Mode to "fix" double rendering. It helps catch real bugs before production.


Summary: Rendering Debugging Workflow

Is the screen blank?
├─ Yes → Check console for errors → Add Error Boundaries
└─ No
   ├─ Is the UI flickering?
   │  └─ Check for hydration mismatches → useEffect for client-only values
   ├─ Is the UI sluggish?
   │  ├─ Profiler shows too many renders → React.memo / useCallback
   │  ├─ Large list rendering → Virtualization
   │  └─ Memory growing → Check effect cleanup
   └─ Is there an infinite loop?
      └─ Check for state updates in render body → Move to effects/handlers

On this page