[SOLVED] Nextjs SSG Suspense throws Hydration Error

Issue

This Content is from Stack Overflow. Question asked by Lee Harrison

I’ve got a static site and I’m trying to render a simple list of items retrieved from Supabase. I had this working in React 17, but with React 18 it throws this error sporadically, and the fallback doesn’t reliably appear. This seems to mostly happen when doing a hard page refresh. Authentication is done via cookie and server-side middleware.

Error: This Suspense boundary received an update before it finished hydrating. This caused the boundary to switch to client rendering. The usual way to fix this is to wrap the original update in startTransition.

//index.tsx (page component)

import { AddEventButton } from '@/components/index';
import { ComponentWithLayout } from '@/types/definitions';
import { Suspense } from 'react';
import { getNavbarLayout } from '@/layouts/NavbarLayout';
import UpcomingEventListSkeleton from '@/components/upcomingEventList/UpcomingEventList.skeleton';
import dynamic from 'next/dynamic';

const UpcomingEventList = dynamic(
  () => import('@/components/upcomingEventList/UpcomingEventList'),
  { suspense: true }
);

const UpcomingEventsPage: ComponentWithLayout = () => {
  return (
    <div className="mx-auto lg:w-1/2 xs:w-full">
      <div className="space-y-2">
        <Suspense fallback={<UpcomingEventListSkeleton />}>
          <UpcomingEventList />
        </Suspense>
      </div>
      <AddEventButton />
    </div>
  );
};

UpcomingEventsPage.getLayout = getNavbarLayout;

export default UpcomingEventsPage;
//UpcomingEventList.tsx

import { CalendarEvent } from '@/types/definitions';
import { CalendarEventConfiguration } from '@/utils/appConfig';
import { UpcomingEvent } from './components/upcomingEvent/UpcomingEvent';
import { supabaseClient } from '@supabase/auth-helpers-nextjs';
import React from 'react';
import dayjs from 'dayjs';
import useSWR from 'swr';

/**
 *  Shows a list of Upcoming Event components
 * @returns UpcomingEventList component
 */
export const UpcomingEventList = () => {
  const { data } = useSWR(
    'upcomingEvents',
    async () =>
      await supabaseClient
        .from<CalendarEvent>(CalendarEventConfiguration.database.tableName)
        .select('*, owner: user_profile(full_name, avatar)')
        .limit(10)
        .order('end', { ascending: true })
        .gt('end', dayjs(new Date()).toISOString()),
    {
      refreshInterval: 3000,
      suspense: true,
    }
  );

  return (
    <>
      {data && data.data?.length === 0 && (
        <div className="card shadow bg-base-100  mx-auto w-full border text-center p-5">
          <div className="text-base-content font-medium">
            No upcoming events scheduled
          </div>
          <div className="text-sm opacity-50">
            Events can be scheduled by clicking the purple button in the
            bottom-right corner.
          </div>
        </div>
      )}
      {data &&
        data.data?.map(
          ({ id, owner, start, end, number_of_guests, privacy_requested }) => (
            <UpcomingEvent
              key={id}
              title={owner?.full_name}
              startDate={start}
              endDate={end}
              numberOfGuests={number_of_guests}
              privacyRequested={privacy_requested}
              avatarSrc={owner?.avatar}
            />
          )
        )}
    </>
  );
};

export default UpcomingEventList;
//UpcomingEventListSkeleton.tsx

import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
  faEye,
  faPlayCircle,
  faStopCircle,
  faUser,
} from '@fortawesome/free-solid-svg-icons';
import React from 'react';

const UpcomingEventListSkeleton = () => (
  <>
    {[...Array(5).keys()].map((v, i) => (
      <UpcomingEventSkeleton key={i} />
    ))}
  </>
);

export default UpcomingEventListSkeleton;

const UpcomingEventSkeleton = () => (
  <div className="card shadow bg-base-100 text-base-content font-medium mx-auto w-full border">
    <div className="flex items-center m-2 animate-pulse">
      <div className="mr-5 ml-1">
        <div className="rounded-full bg-gray-200 h-12 w-12"></div>
      </div>
      <span className="space-y-1">
        <div className="text-xl">
          <div className="h-8 bg-gray-200 rounded w-56"></div>
        </div>
        <div
          className="text-sm opacity-50"
          style={{
            whiteSpace: 'nowrap',
            textOverflow: 'ellipsis',
            overflow: 'hidden',
            width: '20em',
          }}
        >
          <div className="grid grid-cols-10 grid-rows-4">
            <div>
              <FontAwesomeIcon icon={faPlayCircle} className="col-span-1" />
            </div>
            <div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
            <div>
              <FontAwesomeIcon icon={faStopCircle} />
            </div>
            <div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
            <div>
              <FontAwesomeIcon icon={faUser} />
            </div>
            <div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
            <div>
              <FontAwesomeIcon icon={faEye} />
            </div>
            <div className="h-2 bg-gray-200 rounded col-span-9 mt-1 w-36"></div>
          </div>
        </div>
      </span>
    </div>
  </div>
);

Error Message

I’ve tried swapping the components around, storing the results via useEffect to force client-side rendering, and nothing seems to make this error go away or even change behavior. A simple example that removes the custom components and replaces them with simple strings suffers the exact same issue.

I don’t understand how useTransition would help me here, nor how I would even make use of it with useSWR as my fetching mechanism. I’ve searched around and most people who have this issue seem to be using SSR, and not SSG as I am.

Any help is appreciated.

Solution

The cause of my issue was useSwr was making calls before the @supabase/auth-helpers-nextjs user object had fully initialized. This was causing multiple requests to be fired, some authorized, others not, and was making the data object flip rapidly between multiple null | hydrated states. Once I added a conditional to useSwr, it stopped the calls and Suspense began working correctly.

import { CalendarEvent } from '@/types/definitions';
import { CalendarEventConfiguration } from '@/utils/appConfig';
import { UpcomingEvent } from './components/upcomingEvent/UpcomingEvent';
import { supabaseClient } from '@supabase/auth-helpers-nextjs';
import { useUser } from '@supabase/auth-helpers-react';
import React from 'react';
import dayjs from 'dayjs';
import useSWR from 'swr';

/**
 *  Shows a list of Upcoming Event components
 * @returns UpcomingEventList component
 */
export const UpcomingEventList = () => {
  const user = useUser();   // <--- get user via hook
  const { data } = useSWR(
    user && !user.isLoading ? 'upcomingEventsList' : null, // <--- make SWR conditional on user being available
    async () =>
      await supabaseClient
        .from<CalendarEvent>(CalendarEventConfiguration.database.tableName)
        .select('*, owner: user_profile(full_name, avatar)')
        .limit(10)
        .order('end', { ascending: true })
        .gt('end', dayjs(new Date()).toISOString()),
    { refreshInterval: 10000, suspense: true }
  );

  return (
    <>
      {data?.data && !data.data.length && (
        <div className="card shadow bg-base-100  mx-auto w-full border text-center p-5">
          <div className="text-base-content font-medium">
            No upcoming events scheduled
          </div>
          <div className="text-sm opacity-50">
            Events can be scheduled by clicking the purple button in the
            bottom-right corner.
          </div>
        </div>
      )}
      {data?.data?.map(
        ({ id, owner, start, end, number_of_guests, privacy_requested }) => (
          <UpcomingEvent
            key={id}
            title={owner?.full_name}
            startDate={start}
            endDate={end}
            numberOfGuests={number_of_guests}
            privacyRequested={privacy_requested}
            avatarSrc={owner?.avatar}
          />
        )
      )}
    </>
  );
};

export default UpcomingEventList;

This worked in my case, but I’d wager that other encountering this error may have similar request patterns that cause Suspense to perform poorly, or break altogether.

SWR was somewhat masking the issue, and by removing and testing the calls with useState and useEffect, I was able to narrow down the problem to the SWR call, and finally the missing user object.

For more tutorials visit Jtuto.com


This Question and Answer are collected from stackoverflow and tested by JTuto community, is licensed under the terms of CC BY-SA 4.0.

people found this article helpful. What about you?