How to use a server-component on the client? (nextjs)

I’m a bit confused how to make Nextjs paradigm work with pre-rendering something in a server component via Apollo. I get an error when I try to employ this approach:

'use client';

import {useSelectedLayoutSegments} from 'next/navigation';
import {useLocale} from 'next-intl';
import dynamic from 'next/dynamic';

const BreadcrumbsRSC = dynamic(() => import('./Breadcrumbs.server'), {ssr: true});

export default function Breadcrumbs() {
  const segments = useSelectedLayoutSegments();
  const locale = useLocale();
  return <BreadcrumbsRSC segments={segments ?? []} locale={locale as any} />;
}

This is supposed to load/use a breadcrumb that is generated on the server (no need to read code, point is, this server component does some API shenanigans and renders a breadcrumb):

import { breadcrumbLabels } from '@/i18n/pathnames';
import { getClient } from '@/lib/client';
import { gql } from '@apollo/client';
import { UrlLocale, supportedLocales } from '@/i18n/settings';
import { categorySlugs } from '@/i18n/pathnames';

import Link from 'next/link';
import { headers } from 'next/headers'
import {getLocale} from 'next-intl/server';


// GraphQL query used when resolving business slug
const GET_BUSINESS_NAME = gql`
  query GetBusinessName($slug: String!, $language: LanguageCodeEnum) {
    business(slug: $slug, language: $language) {
      name
    }
  }
`;

// Helper: capitalise words for fallback labels
declare function capitalizeWords(str: string): string;
function capitalizeWords(str: string): string {
  return str
    .replace(/-/g, ' ')
    .replace(/\b\w/g, (c) => c.toUpperCase());
}

// Resolve a dynamic segment label. We try, in order:
// 1. Category slug mapping
// 2. Business GraphQL lookup
// 3. Fallback humanised slug
async function resolveDynamicLabel(
  locale: UrlLocale,
  slug: string
): Promise<string> {
  // 1. Category slug?
  const matchedKey = Object.keys(categorySlugs).find(
    (key) => categorySlugs[key as keyof typeof categorySlugs][locale] === slug
  );
  if (matchedKey) {
    return capitalizeWords(matchedKey);
  }

  // 2. Business?
  // Convert url locale (en-us) to canonical (en_us) used by backend
  const canonical =
    supportedLocales.find((l) => l.url === locale)?.canonical || 'en_us';
  try {
    const { data } = await getClient().query({
      query: GET_BUSINESS_NAME,
      variables: { slug, language: canonical },
      context: { fetchOptions: { next: { revalidate: 3600 } } },
    });
    if (data?.business?.name) return data.business.name as string;
  } catch {
    /* ignore – treat as unknown slug */
  }

  // 3. Fallback
  return capitalizeWords(slug);
}

export default async function BreadcrumbsServer({
  segments = [],
  locale,
}: {
  segments?: string[] | null;
  locale: UrlLocale;
}) {
  // const headersList = await headers()
  // console.log(headers);
  // const pathname = headersList.get('x-pathname') || 
  //                  headersList.get('x-invoke-path') ||
  //                  new URL(headersList.get('referer') || '').pathname
  
  // const safeSegments = pathname.split('/').filter(Boolean)
  // locale = await getLocale();
  

  const safeSegments = segments ?? [];
  // Build incremental hrefs so that we can compare against pathname patterns
  const parts: { href: string; slug: string }[] = [];
  let href = '';
  for (const seg of safeSegments) {
    href += `/${seg}`;
    parts.push({ href, slug: seg });
  }

  // Resolve labels in parallel
  const labels: string[] = await Promise.all(
    parts.map(async ({ href: partHref, slug }) => {
      // 1. Static label lookup by full path (root already stripped off locale)
      const staticLabelEntry = breadcrumbLabels[partHref as keyof typeof breadcrumbLabels];
      if (staticLabelEntry && staticLabelEntry[locale]) {
        return staticLabelEntry[locale];
      }
      // 2. Dynamic resolution chain
      return await resolveDynamicLabel(locale, slug);
    })
  );

  if (parts.length === 0) return null;

  return (
    <nav className="mb-6 text-sm">
      {parts.map((p, i) => (
        <span key={p.href}>
          {i < parts.length - 1 ? (
            <Link href={`/${locale}${p.href}`}>{labels[i]}</Link>
          ) : (
            <span aria-current="page">{labels[i]}</span>
          )}
          {i < parts.length - 1 && ' / '}
        </span>
      ))}
    </nav>
  );
}

This approach throws an error:

The export registerApolloClient was not found in module [project]/node_modules/@apollo/client-integration-nextjs/dist/index.browser.js [app-client] (ecmascript) <exports>.
Did you mean to import ApolloClient?
All exports of the module are statically known (It doesn't have dynamic exports). So it's known statically that the requested export doesn't exist.

Which seems to be when you try to use getclient/server apollo approach on a client component… which, I guess makes sense, I’m importing it in a client component…

The reason why I wanted to do that, is to use it on a page that is a client component.

So, I’m a bit confused how do you actually use a component such as my breadcrumb on any page/component, whether it’s server-side or client-side? I need to/want to dynamically construct the breadcrumb on the server (since it has several dynamic data with titles/slugs).

Is the only way to do this to embed the breadcrumb on a high-level server-side page, and then pass it to the client component in which I want to render it?

Such as this?

export default async function DirectoryPage({ params }: DirectoryPageProps) {
  const { locale } = await params;

  return (

    <div>

    <Breadcrumbs
    />
    <DirectoryPageComponent 
    breadcrumbs={<Breadcrumbs />} 
    locale={locale} />

    </div>
  )
}

This works but it’s a bit verbose.

I’m sorry, I know this is maybe more a Next.js than an Apollo question… But I wasn’t sure if this is expected behavior with the apollo error. Is the pattern that I’d prefer impossible? I read through the docs on the apollo-next-integration GitHub but I wasn’t really sure, especially since I’m still brand-new to server-side components.

Thanks for the suggestions!

More of a React/Next question than an Apollo Client question, but I’ll try to answer it :slight_smile:

This might be the main point of confusion here - SSR and RSC are different things.

“SSR” means “rendering on the server” - and every client component will also do that by default, while “RSC” is a completely different type of component that renders on the server in a separate render run, before any Client Components ever get the chance to render (even before they SSR).

If a component is RSC is only defined by one thing: is it imported by another RSC and not marked “use client”?
Every component you import from a “use client” file will always be a Client Component (although it might SSR), but you’ll never be able to “go back to RSC” - the RSC run already happened way earlier in time.

So yes, your composition approach here, passing an RSC as a prop (or child) into a Client Component is the only way in React to render a React Server Component is the only way to do this - you’re on the right track there.

1 Like

Thanks, very insightful! Cool, then I’ll go down that route!

I finally understand why some devs are complaining that Frontend has become too complex :joy:

This is just something horribly underdocumented on the Next.js site :frowning:

I actually got the code snippet and similar ones to embed the server component in the client component by AI and they always thought that’s fine… Multiple models suggested approaches like this that didn’t work, and I had to research and instruct/ask “Wait, shouldn’t it be done like this?”…

Not sure who’s to blame, Next docs, AI, me :smiley: I probably could have figured it out if I spent a day or so reading through NextJS docs/tutorials and grasped RSC better.

I think this should also be clarified in the Apollo docs for next-integration.

Right now they are quite basic and I’m winging it by trial and erroring my way through what the best way is to use the library.

The error code above is relevant for this GH issue as well.

If this GH issue wouldn’t have existed, I definitely would have never figured out that the whole approach the models suggested cannot work.

The main problem we have here: is it our responsibility to teach React Server Components to people, or should we assume that people know the underlying tech and just want our library to work with these paradigms?

If we chose to explain the “what RSCs are”, we would end up repeating 90% of upstream documentation and run the risk of running out of date with said documentation - and our actual usage instructions would become a lot harder to parse for people who would want to actually just look something up.
(And as complex as the topic is, we would also never be “complete” in our attempt to document it.)

I’m not saying that I’m happy with this state of things, but I don’t have a really good solution - it’s an education problem that should be addressed before people even touch our docs for the first time, tbh. :confused:

I think a somewhat bigger sample app would be enough for most people to understand how the entire integration works. The example app linked is a bit small (and old, I think it didn’t even include latest syntax for some things), and I don’t think covers everything in the GH docs.

The docs on the GitHub repo are okay, but it would be nice to see how everything would look in practice.

E. g. so far, I’m pretty much clear on how useQuery (old way), useSuspenseQuery and server-side getClient work. But e. g. I don’t know yet (and haven’t tried yet) how to re-fetch data from a useSuspense query. I believe the lower parts of the docs explain it, but seeing it in action is easier than reading a conceptual doc.

Yeah, we have that on the list :slight_smile: Just always too many things, too little time :confused:

General recommendation: use useSuspenseQuery over useQuery, as useQuery cannot SSR.
Apart from that, useSuspenseQuery is really an Apollo Client core API, so I recommend the documentation for that: Suspense - Apollo GraphQL Docs

1 Like