Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: wrap header and footer in suspense #1388

Merged
merged 1 commit into from
Sep 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/thick-donuts-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bigcommerce/catalyst-core": minor
---

wraps header and footer in suspense boundaries
39 changes: 9 additions & 30 deletions core/app/[locale]/(default)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,31 @@
import { unstable_setRequestLocale } from 'next-intl/server';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, Suspense } from 'react';

import { getSessionCustomerId } from '~/auth';
import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { Footer } from '~/components/footer/footer';
import { FooterFragment } from '~/components/footer/fragment';
import { Header } from '~/components/header';
import { Header, HeaderSkeleton } from '~/components/header';
import { Cart } from '~/components/header/cart';
import { HeaderFragment } from '~/components/header/fragment';
import { LocaleType } from '~/i18n/routing';

interface Props extends PropsWithChildren {
params: { locale: LocaleType };
}

const LayoutQuery = graphql(
`
query LayoutQuery {
site {
...HeaderFragment
...FooterFragment
}
}
`,
[HeaderFragment, FooterFragment],
);

export default async function DefaultLayout({ children, params: { locale } }: Props) {
export default function DefaultLayout({ children, params: { locale } }: Props) {
unstable_setRequestLocale(locale);

const customerId = await getSessionCustomerId();

const { data } = await client.fetch({
document: LayoutQuery,
fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } },
});

return (
<>
<Header cart={<Cart />} data={data.site} />
<Suspense fallback={<HeaderSkeleton />}>
<Header cart={<Cart />} />
</Suspense>

<main className="flex-1 px-4 2xl:container sm:px-10 lg:px-12 2xl:mx-auto 2xl:px-0">
{children}
</main>

<Footer data={data.site} />
<Suspense>
<Footer />
</Suspense>
</>
);
}
15 changes: 15 additions & 0 deletions core/app/[locale]/(default)/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { graphql } from '~/client/graphql';
import { FooterFragment } from '~/components/footer/fragment';
import { HeaderFragment } from '~/components/header/fragment';

export const LayoutQuery = graphql(
`
query LayoutQuery {
site {
...HeaderFragment
...FooterFragment
}
}
`,
[HeaderFragment, FooterFragment],
);
30 changes: 15 additions & 15 deletions core/app/[locale]/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { removeEdgesAndNodes } from '@bigcommerce/catalyst-client';
import { ShoppingCart } from 'lucide-react';
import { getTranslations } from 'next-intl/server';
import { Suspense } from 'react';

import { client } from '~/client';
import { graphql } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { Footer } from '~/components/footer/footer';
import { FooterFragment } from '~/components/footer/fragment';
import { Header } from '~/components/header';
import { Header, HeaderSkeleton } from '~/components/header';
import { CartLink } from '~/components/header/cart';
import { HeaderFragment } from '~/components/header/fragment';
import { ProductCardFragment } from '~/components/product-card/fragment';
import { ProductCardCarousel } from '~/components/product-card-carousel';
import { SearchForm } from '~/components/search-form';
Expand All @@ -18,8 +17,6 @@ const NotFoundQuery = graphql(
`
query NotFoundQuery {
site {
...HeaderFragment
...FooterFragment
featuredProducts(first: 4) {
edges {
node {
Expand All @@ -30,7 +27,7 @@ const NotFoundQuery = graphql(
}
}
`,
[HeaderFragment, FooterFragment, ProductCardFragment],
[ProductCardFragment],
);

export default async function NotFound() {
Expand All @@ -45,14 +42,15 @@ export default async function NotFound() {

return (
<>
<Header
cart={
<CartLink>
<ShoppingCart aria-label="cart" />
</CartLink>
}
data={data.site}
/>
<Suspense fallback={<HeaderSkeleton />}>
<Header
cart={
<CartLink>
<ShoppingCart aria-label="cart" />
</CartLink>
}
/>
</Suspense>

<main className="mx-auto mb-10 max-w-[835px] space-y-8 px-4 sm:px-10 lg:px-0">
<div className="flex flex-col gap-8 px-0 py-16">
Expand All @@ -68,7 +66,9 @@ export default async function NotFound() {
/>
</main>

<Footer data={data.site} />
<Suspense>
<Footer />
</Suspense>
</>
);
}
Expand Down
19 changes: 14 additions & 5 deletions core/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import {
} from '@icons-pack/react-simple-icons';
import { JSX } from 'react';

import { FragmentOf } from '~/client/graphql';
import { LayoutQuery } from '~/app/[locale]/(default)/query';
import { getSessionCustomerId } from '~/auth';
import { client } from '~/client';
import { readFragment } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { Footer as ComponentsFooter } from '~/components/ui/footer';
import { logoTransformer } from '~/data-transformers/logo-transformer';

Expand All @@ -31,11 +35,16 @@ const socialIcons: Record<string, { icon: JSX.Element }> = {
YouTube: { icon: <SiYoutube title="YouTube" /> },
};

interface Props {
data: FragmentOf<typeof FooterFragment>;
}
export const Footer = async () => {
const customerId = await getSessionCustomerId();

const { data: response } = await client.fetch({
document: LayoutQuery,
fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } },
});

const data = readFragment(FooterFragment, response).site;

export const Footer = ({ data }: Props) => {
const sections = [
{
title: 'Categories',
Expand Down
40 changes: 36 additions & 4 deletions core/components/header/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ import { ShoppingCart, User } from 'lucide-react';
import { getLocale, getTranslations } from 'next-intl/server';
import { ReactNode, Suspense } from 'react';

import { LayoutQuery } from '~/app/[locale]/(default)/query';
import { getSessionCustomerId } from '~/auth';
import { FragmentOf } from '~/client/graphql';
import { client } from '~/client';
import { readFragment } from '~/client/graphql';
import { revalidate } from '~/client/revalidate-target';
import { logoTransformer } from '~/data-transformers/logo-transformer';
import { localeLanguageRegionMap } from '~/i18n/routing';

Expand All @@ -19,15 +22,20 @@ import { QuickSearch } from './quick-search';

interface Props {
cart: ReactNode;
data: FragmentOf<typeof HeaderFragment>;
}

export const Header = async ({ cart, data }: Props) => {
export const Header = async ({ cart }: Props) => {
const locale = await getLocale();
const t = await getTranslations('Components.Header');

const customerId = await getSessionCustomerId();

const { data: response } = await client.fetch({
document: LayoutQuery,
fetchOptions: customerId ? { cache: 'no-store' } : { next: { revalidate } },
});

const data = readFragment(HeaderFragment, response).site;

/** To prevent the navigation menu from overflowing, we limit the number of categories to 6.
To show a full list of categories, modify the `slice` method to remove the limit.
Will require modification of navigation menu styles to accommodate the additional categories.
Expand Down Expand Up @@ -97,3 +105,27 @@ export const Header = async ({ cart, data }: Props) => {
/>
);
};

export const HeaderSkeleton = () => (
<header className="flex min-h-[92px] animate-pulse items-center justify-between gap-1 overflow-y-visible bg-white px-4 2xl:container sm:px-10 lg:gap-8 lg:px-12 2xl:mx-auto 2xl:px-0">
<div className="h-16 w-40 rounded bg-slate-200" />
<div className="hidden space-x-4 lg:flex">
<div className="h-6 w-20 rounded bg-slate-200" />
<div className="h-6 w-20 rounded bg-slate-200" />
<div className="h-6 w-20 rounded bg-slate-200" />
<div className="h-6 w-20 rounded bg-slate-200" />
</div>
<div className="flex items-center gap-2 lg:gap-4">
<div className="h-8 w-8 rounded-full bg-slate-200" />

<div className="flex gap-2 lg:gap-4">
<div className="h-8 w-8 rounded-full bg-slate-200" />
<div className="h-8 w-8 rounded-full bg-slate-200" />
</div>

<div className="h-8 w-20 rounded bg-slate-200" />

<div className="h-8 w-8 rounded bg-slate-200 lg:hidden" />
</div>
</header>
);
Loading