Agent Skill · Salesforce Commerce Cloud

sfnext-components

Build UI components in Storefront Next using createPage HOC, Suspense/Await patterns, shadcn/ui, and Tailwind CSS v4. Use when creating page components, adding Suspense boundaries, integrating shadcn/ui, styling with Tailwind, or organizing component files. Covers server vs client rendering patterns and the cn() utility.

Provider: Salesforce Commerce Cloud Path in repo: skills/storefront-next/skills/sfnext-components/SKILL.md

Skill body

Components Skill

This skill covers component development patterns in Storefront Next — createPage HOC, Suspense boundaries, shadcn/ui integration, and Tailwind CSS styling.

Page Component Pattern

Most routes export a default function component that receives loaderData as props:

import { Suspense } from 'react';
import { Await } from 'react-router';
import { SeoMeta } from '@/components/seo-meta';

type ProductPageData = {
    product: Promise<Product>;
    reviews: Promise<Reviews>;
};

export default function ProductPage({ loaderData }: { loaderData: ProductPageData }) {
    return (
        <>
            <SeoMeta title="Product" />
            <Suspense fallback={<ProductHeaderSkeleton />}>
                <Await resolve={loaderData.product}>
                    {(product) => <ProductHeader product={product} />}
                </Await>
            </Suspense>
            <Suspense fallback={<ReviewsSkeleton />}>
                <Await resolve={loaderData.reviews}>
                    {(reviews) => <ProductReviews reviews={reviews} />}
                </Await>
            </Suspense>
        </>
    );
}

createPage HOC (Optional)

For pages needing standardized Suspense wrappers and page key management:

import { createPage } from '@/components/create-page';

export default createPage({
    component: ProductView,
    fallback: <ProductSkeleton />,
});

Suspense Boundaries and Code Splitting

Use <Suspense> + <Await> for streaming loader data, and lazy() for code-splitting heavy components:

import { lazy, Suspense } from 'react';

// Code-split a heavy component
const CustomerReviewsSection = lazy(() =>
    import('@/components/customer-reviews-section/customer-reviews-section')
);

export default function ProductPage({ loaderData }: { loaderData: ProductPageData }) {
    return (
        <>
            {/* Stream loader data */}
            <Suspense fallback={<ProductHeaderSkeleton />}>
                <Await resolve={loaderData.product}>
                    {(product) => <ProductHeader product={product} />}
                </Await>
            </Suspense>

            {/* Code-split component */}
            <Suspense fallback={<ReviewsSkeleton />}>
                <CustomerReviewsSection productId={loaderData.productId} />
            </Suspense>
        </>
    );
}

shadcn/ui Components

shadcn/ui provides pre-built accessible components. Add them via CLI:

npx shadcn@latest add button
npx shadcn@latest add card
npx shadcn@latest add dialog

Rules:

// Optional wrapper for app-specific styling
import { Button } from '@/components/ui/button';

export function PrimaryButton(props: React.ComponentProps<typeof Button>) {
    return <Button {...props} className={cn('rounded-full', props.className)} />;
}

Tailwind CSS Styling

Tailwind CSS v4 is the only permitted styling approach:

import { cn } from '@/lib/utils';

export function ProductCard({ featured }: { featured?: boolean }) {
    return (
        <div className={cn(
            'rounded-lg border border-border bg-card p-4',
            featured && 'ring-2 ring-primary'
        )}>
            <h2 className="text-lg font-semibold text-card-foreground">
                Product Name
            </h2>
        </div>
    );
}

Rules:

Dark Mode

Theme variables automatically adapt via CSS variables:

<div className="bg-background text-foreground border-border">
    <button className="bg-primary text-primary-foreground">Click me</button>
</div>

Responsive Design

<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
    {products.map(p => <ProductCard key={p.id} product={p} />)}
</div>

File Organization

src/components/product-tile/
├── index.tsx              # Component implementation
├── index.test.tsx         # Vitest unit tests
└── stories/
    ├── index.stories.tsx  # Storybook stories
    └── __snapshots__/     # Storybook snapshots (optional)

src/components/product-skeleton/
├── index.tsx              # Skeleton component (separate from main)
├── index.test.tsx
└── stories/
    └── index.stories.tsx

Best Practices

  1. Export default function components — Receive loaderData as props
  2. Granular Suspense boundaries — Show content progressively as data resolves
  3. Use lazy() for heavy components — Code-split below-the-fold or conditional UI
  4. Reusable skeleton components — Consistent loading states
  5. Colocate tests and stories — Keep test files next to source files
  6. TypeScript interfaces — Define proper types for all props