search
React star Featured

Error: Next.js & React.js Text content does not match server-rendered HTML

Learn how to resolve the 'Text content does not match server-rendered HTML' error in React. Complete guide with solutions for server-side rendering and hydration issues.

person By Gautam Sharma
calendar_today January 2, 2026
schedule 14 min read
React SSR Server-Side Rendering Hydration Error Handling Next.js

The ‘Text content does not match server-rendered HTML’ error is a common issue developers encounter when implementing server-side rendering (SSR) in React applications. This error occurs when there’s a mismatch between the HTML rendered on the server and what React expects during client-side hydration.

This comprehensive guide provides complete solutions to resolve the text content mismatch error with practical examples and SSR optimization techniques.


Understanding the Text Content Mismatch Error

React’s hydration process reconciles the server-rendered HTML with the client-side React component tree. When there’s a mismatch between server and client content, React warns about the inconsistency to maintain proper functionality.

Common Error Messages:

  • Text content does not match server-rendered HTML
  • Warning: Text content did not match
  • Server: "Loading..." Client: "Hello World"
  • Hydration failed because the initial UI does not match what was rendered on the server

Common Causes and Solutions

1. Conditional Rendering Based on Environment

The most common cause is rendering different content on server vs client based on environment detection.

❌ Problem Scenario:

// This will cause a mismatch
function BadComponent() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    // This only runs on client
    setIsClient(true);
  }, []);

  return (
    <div>
      {/* ❌ Server renders "Loading..." but client renders "Client Side" */}
      {isClient ? 'Client Side Content' : 'Loading...'}
    </div>
  );
}

✅ Solution: Use useEffect for Client-Side Updates

// Correct approach - render same content initially
function GoodComponent() {
  const [content, setContent] = useState('Loading...');

  useEffect(() => {
    // Update content after hydration
    setContent('Client Side Content');
  }, []);

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

// Alternative: Use a client-side only component
function ClientOnly({ children }) {
  const [isMounted, setIsMounted] = useState(false);

  useEffect(() => {
    setIsMounted(true);
  }, []);

  return isMounted ? children : null;
}

function BetterComponent() {
  return (
    <div>
      <ClientOnly>
        <p>Client Side Content</p>
      </ClientOnly>
    </div>
  );
}

2. Time-Based Content Differences

Rendering different content based on time can cause mismatches.

❌ Problem Scenario:

// This will cause a mismatch if server and client times differ
function BadTimeComponent() {
  const [currentTime, setCurrentTime] = useState(new Date().toLocaleTimeString());

  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentTime(new Date().toLocaleTimeString());
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>Current Time: {currentTime}</div>;
}

✅ Solution: Initialize with Server-Safe Values

// Correct approach - use server-safe initial value
function GoodTimeComponent() {
  const [currentTime, setCurrentTime] = useState(() => {
    // Use a placeholder that works on both server and client
    if (typeof window !== 'undefined') {
      return new Date().toLocaleTimeString();
    }
    return 'Loading...'; // Server-safe placeholder
  });

  useEffect(() => {
    const updateTimer = () => {
      setCurrentTime(new Date().toLocaleTimeString());
    };

    updateTimer(); // Update immediately on client
    const timer = setInterval(updateTimer, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>Current Time: {currentTime}</div>;
}

Solution 1: Proper SSR-Safe Conditional Rendering

Implement conditional rendering that works consistently on both server and client.

// SSR-safe conditional rendering
function SSRSafeComponent() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <div>
      {/* ✅ Render the same initial content */}
      <p>{isClient ? 'Client Environment' : 'Server Environment'}</p>
      
      {/* ✅ Use conditional rendering that doesn't cause mismatches */}
      {isClient && <div>This only renders on client</div>}
      
      {/* ✅ Or use a more sophisticated approach */}
      <SSRConditional>
        <ClientContent />
      </SSRConditional>
    </div>
  );
}

// Custom component for SSR-safe conditional rendering
function SSRConditional({ children }) {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return isClient ? children : <div>Loading...</div>;
}

Solution 2: Using Next.js Built-in Solutions

Leverage Next.js features to handle SSR mismatches.

// Next.js dynamic imports with SSR disabled
import dynamic from 'next/dynamic';

// ✅ Dynamic import with SSR disabled
const ClientOnlyComponent = dynamic(
  () => import('../components/ClientOnlyComponent'),
  { ssr: false }
);

// ✅ Dynamic import with loading component
const LazyComponent = dynamic(
  () => import('../components/LazyComponent'),
  {
    loading: () => <p>Loading...</p>,
    ssr: false
  }
);

// Next.js page component
export default function MyPage() {
  return (
    <div>
      <h1>Server-rendered content</h1>
      <ClientOnlyComponent />
      <LazyComponent />
    </div>
  );
}

// Using Next.js router for client-side detection
import { useRouter } from 'next/router';

function RouterBasedComponent() {
  const router = useRouter();
  const [isReady, setIsReady] = useState(false);

  useEffect(() => {
    if (router.isReady) {
      setIsReady(true);
    }
  }, [router.isReady]);

  return (
    <div>
      {isReady ? (
        <p>Client-side content</p>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

Solution 3: Custom Hooks for SSR Detection

Create custom hooks to handle server/client detection safely.

// Custom hook for SSR-safe environment detection
function useIsomorphicLayoutEffect() {
  return typeof window !== 'undefined' 
    ? useEffect 
    : (callback) => callback();
}

// Hook to detect client-side environment
function useIsClient() {
  const [isClient, setIsClient] = useState(false);

  useIsomorphicLayoutEffect(() => {
    setIsClient(true);
  }, []);

  return isClient;
}

// Component using the custom hook
function IsomorphicComponent() {
  const isClient = useIsClient();

  return (
    <div>
      <p>Environment: {isClient ? 'Client' : 'Server'}</p>
      {isClient && <div>Client-only content</div>}
    </div>
  );
}

// Hook for SSR-safe data fetching
function useSSRData(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const isClient = useIsClient();

  useEffect(() => {
    if (isClient) {
      const fetchData = async () => {
        try {
          const response = await fetch(url);
          const result = await response.json();
          setData(result);
        } catch (error) {
          console.error('Error fetching data:', error);
        } finally {
          setLoading(false);
        }
      };

      fetchData();
    }
  }, [isClient, url]);

  return { data, loading };
}

// Component using SSR-safe data fetching
function SSRDataComponent({ url }) {
  const { data, loading } = useSSRData(url);

  if (loading) {
    return <div>Loading...</div>;
  }

  return <div>{data && <pre>{JSON.stringify(data, null, 2)}</pre>}</div>;
}

Solution 4: Proper State Initialization

Initialize state with values that work on both server and client.

// Proper state initialization for SSR
function ProperStateInit() {
  // ✅ Initialize with server-safe values
  const [userAgent, setUserAgent] = useState(() => {
    if (typeof window !== 'undefined') {
      return window.navigator.userAgent;
    }
    return 'Server'; // Server-safe default
  });

  const [windowSize, setWindowSize] = useState(() => {
    if (typeof window !== 'undefined') {
      return {
        width: window.innerWidth,
        height: window.innerHeight
      };
    }
    return { width: 0, height: 0 }; // Server-safe default
  });

  useEffect(() => {
    // Update values after hydration
    setUserAgent(window.navigator.userAgent);
    
    const handleResize = () => {
      setWindowSize({
        width: window.innerWidth,
        height: window.innerHeight
      });
    };

    handleResize(); // Set initial values
    window.addEventListener('resize', handleResize);

    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <div>
      <p>User Agent: {userAgent}</p>
      <p>Window Size: {windowSize.width} x {windowSize.height}</p>
    </div>
  );
}

// Component with proper date initialization
function ProperDateComponent() {
  const [currentDate, setCurrentDate] = useState(() => {
    if (typeof window !== 'undefined') {
      return new Date().toISOString();
    }
    return new Date().toISOString(); // Same value for both environments
  });

  useEffect(() => {
    const timer = setInterval(() => {
      setCurrentDate(new Date().toISOString());
    }, 1000);

    return () => clearInterval(timer);
  }, []);

  return <div>Current Date: {new Date(currentDate).toLocaleString()}</div>;
}

Solution 5: Error Boundary for Hydration Issues

Use error boundaries to handle hydration errors gracefully.

// Error boundary for hydration issues
import React from 'react';

class HydrationErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }

  static getDerivedStateFromError(error) {
    // Check if this is a hydration error
    if (error.message && error.message.includes('hydration')) {
      return { hasError: true, error };
    }
    return { hasError: false };
  }

  componentDidCatch(error, errorInfo) {
    console.error('Hydration error caught:', error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      return (
        <div className="hydration-error-boundary">
          <h2>Hydration Error Detected</h2>
          <p>There was an issue with server-client content matching.</p>
          <button onClick={() => this.setState({ hasError: false, error: null })}>
            Retry
          </button>
        </div>
      );
    }

    return this.props.children;
  }
}

// Component that might cause hydration issues
function PotentiallyProblematicComponent() {
  const [content, setContent] = useState('Server Content');
  
  useEffect(() => {
    // This might cause hydration mismatch
    setContent('Client Content');
  }, []);

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

// Safe wrapper with error boundary
function SafeHydrationWrapper() {
  return (
    <HydrationErrorBoundary>
      <PotentiallyProblematicComponent />
    </HydrationErrorBoundary>
  );
}

Solution 6: Next.js getServerSideProps and getStaticProps

Use Next.js data fetching methods properly to avoid mismatches.

// pages/ssr-example.js
export async function getServerSideProps() {
  // Server-side data fetching
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();

  return {
    props: {
      serverData: data,
      timestamp: new Date().toISOString() // Server timestamp
    }
  };
}

export default function SSRPage({ serverData, timestamp }) {
  const [clientData, setClientData] = useState(null);
  const [clientTimestamp, setClientTimestamp] = useState(timestamp);

  useEffect(() => {
    // Client-side updates
    setClientTimestamp(new Date().toISOString());
    
    // Fetch client-specific data if needed
    fetch('/api/client-data')
      .then(res => res.json())
      .then(data => setClientData(data));
  }, []);

  return (
    <div>
      <h1>SSR Example</h1>
      <p>Server timestamp: {new Date(timestamp).toLocaleString()}</p>
      <p>Client timestamp: {new Date(clientTimestamp).toLocaleString()}</p>
      <div>
        <h3>Server Data:</h3>
        <pre>{JSON.stringify(serverData, null, 2)}</pre>
      </div>
      {clientData && (
        <div>
          <h3>Client Data:</h3>
          <pre>{JSON.stringify(clientData, null, 2)}</pre>
        </div>
      )}
    </div>
  );
}

// Using getStaticProps with revalidation
export async function getStaticProps() {
  const response = await fetch('https://api.example.com/data');
  const data = await response.json();

  return {
    props: {
      data,
      timestamp: new Date().toISOString()
    },
    revalidate: 60 // Revalidate every 60 seconds
  };
}

export default function StaticPage({ data, timestamp }) {
  const [localData, setLocalData] = useState(data);

  useEffect(() => {
    // Update with client-specific data if needed
    setLocalData(prev => ({
      ...prev,
      clientSpecific: true
    }));
  }, []);

  return (
    <div>
      <h1>Static Generation Example</h1>
      <p>Generated at: {new Date(timestamp).toLocaleString()}</p>
      <pre>{JSON.stringify(localData, null, 2)}</pre>
    </div>
  );
}

Solution 7: React 18 Concurrent Features

Use React 18 features to handle SSR mismatches better.

// Using React 18 Suspense for SSR
import { Suspense } from 'react';

// Component that suspends during data fetching
function AsyncComponent() {
  const data = useAsyncData(); // Custom hook that throws a promise

  return <div>{JSON.stringify(data)}</div>;
}

// Wrapper with Suspense
function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <AsyncComponent />
    </Suspense>
  );
}

// Custom hook for async data with Suspense
function useAsyncData() {
  const [resource, setResource] = useState(null);

  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('/api/data');
      const data = await response.json();
      setResource(data);
    };

    fetchData();
  }, []);

  if (resource === null) {
    throw new Promise(resolve => setTimeout(resolve, 1000)); // Simulate loading
  }

  return resource;
}

// Using React 18's useId for stable IDs
import { useId } from 'react';

function FormWithId() {
  const id = useId();
  
  return (
    <div>
      <label htmlFor={id}>Input Label</label>
      <input id={id} type="text" />
    </div>
  );
}

Solution 8: Testing SSR Components

Create tests to verify SSR compatibility.

// Testing SSR components
import { renderToString } from 'react-dom/server';
import { render, screen, waitFor } from '@testing-library/react';
import { act } from 'react-dom/test-utils';

// Component to test
function SSRTestComponent() {
  const [content, setContent] = useState('Server Content');

  useEffect(() => {
    setContent('Client Content');
  }, []);

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

// Test SSR rendering
describe('SSR Component', () => {
  test('should render without hydration errors', () => {
    // Test server-side rendering
    const serverRendered = renderToString(<SSRTestComponent />);
    expect(serverRendered).toContain('Server Content');
    
    // Test client-side hydration
    render(<SSRTestComponent />);
    
    // Initially should show server content
    expect(screen.getByText('Server Content')).toBeInTheDocument();
    
    // After hydration, should update to client content
    waitFor(() => {
      expect(screen.getByText('Client Content')).toBeInTheDocument();
    });
  });
});

// Mock window object for testing
beforeEach(() => {
  Object.defineProperty(window, 'matchMedia', {
    writable: true,
    value: jest.fn().mockImplementation(query => ({
      matches: false,
      media: query,
      onchange: null,
      addListener: jest.fn(),
      removeListener: jest.fn(),
      addEventListener: jest.fn(),
      removeEventListener: jest.fn(),
      dispatchEvent: jest.fn(),
    })),
  });
});

Solution 9: Performance Optimization for SSR

Optimize SSR performance while maintaining content consistency.

// Performance-optimized SSR component
import { useMemo, useCallback } from 'react';

function OptimizedSSRComponent({ data }) {
  // ✅ Memoize expensive calculations
  const processedData = useMemo(() => {
    if (!data) return null;
    
    // Expensive processing that should be memoized
    return data.map(item => ({
      ...item,
      processed: true,
      id: item.id
    }));
  }, [data]);

  // ✅ Memoize functions to prevent unnecessary re-renders
  const handleClick = useCallback((id) => {
    console.log('Item clicked:', id);
  }, []);

  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  return (
    <div>
      <h2>Optimized SSR Component</h2>
      {isClient ? (
        <p>Client Environment</p>
      ) : (
        <p>Server Environment</p>
      )}
      
      {processedData && (
        <ul>
          {processedData.map(item => (
            <li key={item.id} onClick={() => handleClick(item.id)}>
              {item.name}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}

// Component with proper loading states
function LoadingOptimizedComponent() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    const fetchData = async () => {
      try {
        const response = await fetch('/api/data');
        const result = await response.json();
        setData(result);
      } catch (error) {
        console.error('Error:', error);
      } finally {
        setLoading(false);
      }
    };

    // Only fetch on client
    if (typeof window !== 'undefined') {
      fetchData();
    }
  }, []);

  if (loading) {
    return <div className="skeleton-loader">Loading...</div>;
  }

  return <div>{data && <pre>{JSON.stringify(data, null, 2)}</pre>}</div>;
}

Performance Considerations

Efficient SSR Rendering:

// Optimized SSR patterns
function EfficientSSR() {
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
  }, []);

  // ✅ Use stable initial values
  const [count, setCount] = useState(0);

  // ✅ Memoize expensive operations
  const expensiveValue = useMemo(() => {
    // Only calculate on client
    return isClient ? performExpensiveOperation() : 0;
  }, [isClient]);

  return (
    <div>
      <p>Count: {count}</p>
      <p>Expensive Value: {expensiveValue}</p>
      <button onClick={() => setCount(c => c + 1)}>Increment</button>
      
      {/* ✅ Conditional rendering that doesn't cause mismatches */}
      {isClient && <div>Client-specific content</div>}
    </div>
  );
}

function performExpensiveOperation() {
  // Simulate expensive operation
  let result = 0;
  for (let i = 0; i < 1000000; i++) {
    result += i;
  }
  return result;
}

Security Considerations

Safe SSR Content:

// Secure SSR content handling
function SecureSSRComponent({ userContent }) {
  const [sanitizedContent, setSanitizedContent] = useState('');

  useEffect(() => {
    // Sanitize content on client if needed
    if (userContent) {
      const sanitized = sanitizeContent(userContent);
      setSanitizedContent(sanitized);
    }
  }, [userContent]);

  return (
    <div>
      {/* ✅ Always sanitize user-generated content */}
      <div dangerouslySetInnerHTML={{ __html: sanitizedContent }} />
    </div>
  );
}

function sanitizeContent(content) {
  // Basic sanitization - in production, use a proper library like DOMPurify
  return content
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;');
}

Common Mistakes to Avoid

1. Environment-Specific Rendering:

// ❌ Don't do this
function BadEnvironmentComponent() {
  const [content, setContent] = useState(
    typeof window !== 'undefined' ? 'Client' : 'Server'
  );

  return <div>{content}</div>; // This will cause mismatches
}

2. Time-Based Content:

// ❌ Don't do this
function BadTimeComponent() {
  const [time, setTime] = useState(new Date().toLocaleTimeString());
  
  useEffect(() => {
    const interval = setInterval(() => {
      setTime(new Date().toLocaleTimeString());
    }, 1000);
    return () => clearInterval(interval);
  }, []);

  return <div>{time}</div>; // Server and client will have different times
}

3. Window/Document Access During Render:

// ❌ Don't do this
function BadWindowAccess() {
  const [dimensions, setDimensions] = useState({
    width: window.innerWidth, // ❌ Accessing window during render
    height: window.innerHeight
  });

  return <div>{dimensions.width} x {dimensions.height}</div>;
}

Alternative Solutions

Using React DevTools:

// Component optimized for React DevTools
function DevToolsOptimizedComponent() {
  const [data, setData] = useState(null);
  const [isClient, setIsClient] = useState(false);

  useEffect(() => {
    setIsClient(true);
    // Fetch data after hydration
    fetch('/api/data').then(res => res.json()).then(setData);
  }, []);

  return (
    <div data-testid="ssr-optimized">
      {isClient ? (
        <pre>{JSON.stringify(data, null, 2)}</pre>
      ) : (
        <div>Loading...</div>
      )}
    </div>
  );
}

Feature Detection:

// Check for SSR compatibility
function SSRCompatibleComponent() {
  const [isSSR, setIsSSR] = useState(true);

  useEffect(() => {
    setIsSSR(false); // We're now on the client
  }, []);

  return (
    <div>
      {isSSR ? 'Server' : 'Client'}
    </div>
  );
}

Troubleshooting Checklist

When encountering the text content mismatch error:

  1. Check Conditional Rendering: Ensure server and client render the same initial content
  2. Verify Data Fetching: Confirm data is available during SSR or use loading states
  3. Review Environment Detection: Avoid environment-specific rendering during initial render
  4. Test Hydration: Verify client-side updates don’t conflict with server content
  5. Use React DevTools: Inspect the component tree for mismatch issues
  6. Check Timing: Ensure time-based content is handled properly
  7. Validate Dynamic Imports: Confirm SSR settings for dynamic components

Conclusion

The ‘Text content does not match server-rendered HTML’ error occurs when there’s a mismatch between server and client content during React’s hydration process. By implementing proper SSR patterns, using environment detection safely, and maintaining consistent initial rendering, you can resolve these mismatches and ensure smooth server-side rendering in your React applications.

The key to resolving this error is ensuring that server and client initially render the same content, then allowing client-side updates after hydration. Whether you’re working with Next.js, React Router, or custom SSR implementations, the solutions provided in this guide will help you handle SSR content matching appropriately in your React applications.

Remember to always test your SSR implementations, use proper loading states, and implement safe environment detection to ensure consistent rendering across server and client environments.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

React

How to Fix & Solve React.js and Next.js Hydration Error: Complete Guide 2026

Learn how to fix React.js and Next.js hydration errors with step-by-step solutions. This guide covers client-server mismatch issues, dynamic content rendering, and best practices for seamless SSR.

January 1, 2026
React

How to Fix React app works locally but not after build Error

Learn how to fix React apps that work locally but fail after build. Complete guide with solutions for production deployment and build optimization.

January 2, 2026
React

How to Solve React Blank Page After Deploy & Build Error Tutorial

Learn how to fix React blank page errors after deployment. Complete guide with solutions for production builds and deployment optimization.

January 1, 2026