reactjsnext.jsserver-side-renderinghydrationclient-side-rendering

NextJS: Error: Hydration failed because the initial UI does not match what was rendered on the server


Error

Unhandled Runtime Error Error: Hydration failed because the initial UI does not match what was rendered on the server. See more info here: https://nextjs.org/docs/messages/react-hydration-error

Not sure why I'm getting this error, the following is my Login view route component, and I've tried returning just the <AuthForm />, but also tried 2 different ways of disabling SSR.

'use client';
import dynamic from 'next/dynamic';
// import AuthForm from '@/components/AuthForm';

// const DynamicAuthForm = dynamic(() => import('@/components/AuthForm'), {
//   ssr: false,
// });

const NoSSR = dynamic(() => import('@/components/AuthForm'), { ssr: false });

const LoginPage = () => {
  return (
    <div>
      <NoSSR />;
    </div>
  );
  // return <DynamicAuthForm />;
  // return <AuthForm />;
};

export default LoginPage;

And my AuthForm.tsx component:

'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';

import callFetch from '@/utils/fetch/callFetch';

const apiUrl = process.env.NEXT_PUBLIC_API_DEV_LOGIN || '';

if (!apiUrl) {
  throw new Error('API URL is not defined');
}

function AuthForm() {
  const router = useRouter();
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [isSubmitting, setIsSubmitting] = useState(false);

  // Create a function to handle form submission
  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setIsSubmitting(true);

    const formBody = JSON.stringify({ user: email, password });

    try {
      const response = await callFetch(apiUrl, 'POST', formBody);

      if (!response.ok) {
        setIsSubmitting(false);
        // TODO - handle error in notification alert
        throw new Error(
          `Login failed: ${response.status} - ${response.statusText}`
        );
      }

      console.log('login response:', response);
      setIsSubmitting(false);

      // Redirect to postings upon successful login
      router.push('/postings');
    } catch (error) {
      console.error('Error logging in:', error);
      setIsSubmitting(false);
      // Handle error, show error message, etc.
    }
  };

  return (
    <>
      <div className="login-container">
        {/* <div className="form"> */}
        <form onSubmit={handleSubmit}>
          Login to
          <Link href="/">
            <h1 className="logo">
              Bounty<strong>Jobs</strong>
            </h1>
          </Link>
          <div>
            <label htmlFor="email">Email</label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={e => setEmail(e.target.value)}
              required
            />
          </div>
          <div>
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={e => setPassword(e.target.value)}
              required
            />
          </div>
          <div className="actions">
            <button disabled={isSubmitting}>Submit</button>
          </div>
        </form>
        {/* </div> */}
      </div>
    </>
  );
}

export default AuthForm;

The Form markup

I don't see any issue anywhere in my markup:

return (
    <>
      <div className="login-container">
        {/* <div className="form"> */}
        <form onSubmit={handleSubmit}>
          Login to
          <Link href="/">
            <h1 className="logo">
              Bounty<strong>Jobs</strong>
            </h1>
          </Link>
          <div>
            <label htmlFor="email">Email</label>
            <input
              type="email"
              id="email"
              value={email}
              onChange={e => setEmail(e.target.value)}
              required
            />
          </div>
          <div>
            <label htmlFor="password">Password:</label>
            <input
              type="password"
              id="password"
              value={password}
              onChange={e => setPassword(e.target.value)}
              required
            />
          </div>
          <div className="actions">
            <button disabled={isSubmitting}>Submit</button>
          </div>
        </form>
        {/* </div> */}
      </div>
    </>
  );

Solution

  • The problem was my RootLayout of /login

    Original code:

    export default function RootLayout({
      children,
    }: Readonly<{
      children: React.ReactNode;
    }>) {
      return (
        <html lang="en">
          <body>
            <div>{children}</div>
          </body>
        </html>
      );
    }
    

    I removed the html and body tags here, which probably duplicated themselves since my app layout also has html and body.

    This fixed the issue.