Storefront Next Frontend Development
You're working in a React Server Components (RSC) e-commerce storefront built on React 19, React Router 7, Tailwind CSS 4, and Salesforce Commerce API (SCAPI). This skill gives you the project-specific patterns, conventions, and guardrails for writing correct, idiomatic code in this codebase.
Project Layout
frontend/
├── src/
│ ├── components/ # React components
│ │ ├── ui/ # Radix UI + Tailwind primitives (button, dialog, etc.)
│ │ └── [feature]/ # Feature components with index.tsx, skeleton.tsx, stories/
│ ├── routes/ # File-based React Router routes
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utilities, adapters, decorators, API clients, actions
│ │ ├── adapters/ # Adapter pattern implementations
│ │ ├── decorators/ # Page Designer TypeScript decorators
│ │ ├── api/ # SCAPI client modules
│ │ ├── actions/ # Server actions
│ │ └── utils.ts # cn(), password validation, error extraction
│ ├── providers/ # React context providers (auth, basket, currency, etc.)
│ ├── config/ # Configuration system (schema, utils, getConfig)
│ ├── extensions/ # Optional feature modules (bopis, multiship, store-locator)
│ ├── locales/ # i18n translation files (en-US, it-IT, etc.)
│ └── types/ # Global TypeScript types
├── .storybook/ # Storybook 10 configuration
├── docs/ # Architecture docs (adapters, tests, auth, i18n, perf)
├── config.server.ts # Single source of truth for all app settings
├── vite.config.ts # Vite 7 build config
├── vitest.setup.ts # Test setup (mocks, i18n init, DOM API stubs)
└── package.json # pnpm scripts
Node version: 24+ (managed via .nvmrc — run nvm use before working)
Package manager: pnpm 10.28+ (not npm — that's the backend)
Critical Rules
These cause the most problems when violated:
-
Server components by default. No directive means server component. Only add "use client" when you need hooks, event handlers, or browser APIs. Every "use client" boundary increases the client bundle.
-
No async page loaders. Route loader functions must NOT be async — they return promises directly and React Router handles streaming. This is enforced by ESLint (no-async-page-loader).
-
No client loaders or client actions. All data fetching happens server-side in loader functions. Never use clientLoader or add "use client" to actions. ESLint rules no-client-loaders and no-client-actions enforce this.
-
No hard-coded colors. Use Tailwind design tokens (bg-foreground, text-muted-foreground, border-border), never raw hex/rgb values. The color-linter ESLint rule enforces this.
-
No any types. TypeScript strict mode is on. All code must be properly typed with meaningful types.
-
Apache 2.0 license header required. Every .ts and .tsx file needs the copyright header. ESLint's eslint-plugin-headers enforces this. The year is 2026.
-
Default exports for components, named exports for utilities. This convention enables static registry generation for Page Designer.
-
Use @/ path alias. Import from @/components/..., @/lib/..., @/hooks/... — never relative paths like ../../../.
-
Reuse existing project components. Before importing from a library directly (e.g., lucide-react), check if a project wrapper already exists in src/components/icons/, src/components/ui/, or src/components/buttons/. For example, use HeartIcon from @/components/icons/heart-icon instead of Heart from lucide-react — the project wrapper adds consistent sizing, styling, and accessibility props. Always search the codebase first.
Component Patterns
Standard Component Structure
typescript
1/**
2 * Copyright 2026 Salesforce, Inc.
3 * [... Apache 2.0 header ...]
4 */
5import { useTranslation } from 'react-i18next';
6import { cn } from '@/lib/utils';
7
8export interface ProductCardProps {
9 product: Product;
10 className?: string;
11}
12
13export default function ProductCard({ product, className }: ProductCardProps) {
14 const { t } = useTranslation();
15 return (
16 <div className={cn('rounded-lg border border-border p-4', className)}>
17 <h3 className="text-foreground font-semibold">{product.name}</h3>
18 <p className="text-muted-foreground">{t('product.price', { price: product.price })}</p>
19 </div>
20 );
21}
22
23export function ProductCardSkeleton() {
24 return <div className="animate-pulse rounded-lg border border-border p-4 h-32" />;
25}
File structure for each component:
src/components/product-card/
├── index.tsx # Main component (default export)
├── skeleton.tsx # Loading state (optional)
├── index.test.tsx # Unit tests
└── stories/
└── index.stories.tsx # Storybook story
Client Components
Only use "use client" when you genuinely need interactivity:
typescript
1"use client"
2
3import { useState } from 'react';
4
5export default function QuantitySelector({ initial }: { initial: number }) {
6 const [qty, setQty] = useState(initial);
7 return (
8 <div className="flex items-center gap-2">
9 <button onClick={() => setQty(q => Math.max(1, q - 1))}>-</button>
10 <span>{qty}</span>
11 <button onClick={() => setQty(q => q + 1)}>+</button>
12 </div>
13 );
14}
Styling with Tailwind Design Tokens
typescript
1import { cn } from '@/lib/utils';
2
3// cn() merges Tailwind classes and resolves conflicts
4<div className={cn(
5 'bg-background text-foreground', // Base colors (from design tokens)
6 'border border-border rounded-lg', // Borders
7 'p-4 space-y-2', // Spacing
8 isActive && 'ring-2 ring-ring', // Conditional
9 className // Allow parent overrides
10)} />
Available design tokens: foreground, background, muted-foreground, muted, border, ring, destructive, success, warning, info, secondary, accent, card, popover, primary
UI Primitives
The src/components/ui/ directory contains ~32 Radix UI + Tailwind components. Use these as building blocks — don't rebuild dialogs, dropdowns, tooltips, etc. from scratch. They use class-variance-authority for variants.
Routing
File-based routing in src/routes/ with React Router v7 conventions:
| Pattern | Meaning | Example |
|---|
_app. prefix | Standard layout (header/footer) | _app._index.tsx → / |
_empty. prefix | Empty layout (no chrome) | _empty.login.tsx → /login |
$param | Dynamic segment | _app.product.$productId.tsx → /product/:id |
| Nested dots | Nested routes | _app.account.orders.tsx → /account/orders |
Data Loading Pattern
All data loading happens server-side via loader functions:
typescript
1import type { LoaderFunctionArgs } from 'react-router';
2import { fetchProduct } from '@/lib/api/products';
3
4// NOT async — return promises directly, React Router streams them
5export function loader({ params, context }: LoaderFunctionArgs) {
6 return {
7 product: fetchProduct(context, params.productId),
8 recommendations: fetchRecommendations(context, params.productId),
9 };
10}
11
12export default function ProductPage({ loaderData }: { loaderData: LoaderData }) {
13 return <ProductDetail product={loaderData.product} />;
14}
15
16export function shouldRevalidate() {
17 return false; // Don't re-fetch on navigation unless needed
18}
Configuration System
config.server.ts is the single source of truth. Override any value via environment variables:
bash
1# PUBLIC__ prefix → exposed to browser
2PUBLIC__app__commerce__api__clientId=my-client-id
3PUBLIC__app__features__socialLogin__enabled=true
4
5# No prefix → server-only secrets
6COMMERCE_API_SLAS_SECRET=my-secret
Double underscore __ maps to nested object paths. Deep merging: more specific paths win.
Usage in code:
typescript
1// Server (loaders, actions)
2import { getConfig } from "@/config";
3const config = getConfig(context);
4
5// Client (components)
6import { useConfig } from "@/config";
7const config = useConfig();
Internationalization (i18n)
typescript
1import { useTranslation } from 'react-i18next';
2
3export default function Greeting({ name }: { name: string }) {
4 const { t } = useTranslation();
5 return <h1>{t('greeting.welcome', { name })}</h1>;
6}
- Translation files:
src/locales/[locale]/translations.json
- Extension translations:
src/extensions/[ext]/locales/[locale]/translations.json
- Extension namespace: auto-PascalCase —
store-locator → extStoreLocator
- Fallback language:
en-US
Use React Hook Form + Zod for all form handling:
typescript
1import { useForm } from "react-hook-form";
2import { zodResolver } from "@hookform/resolvers/zod";
3import { z } from "zod";
4
5const schema = z.object({
6 email: z.string().email(),
7 password: z.string().min(8),
8});
9
10type FormValues = z.infer<typeof schema>;
11
12export default function LoginForm() {
13 const {
14 register,
15 handleSubmit,
16 formState: { errors },
17 } = useForm<FormValues>({
18 resolver: zodResolver(schema),
19 });
20 // ...
21}
Extensions System
Modular features in src/extensions/ that can be independently enabled:
- bopis/ — Buy Online, Pick up In Store
- multiship/ — Multi-ship checkout
- store-locator/ — Store location finder
- theme-switcher/ — Dark/light theme toggle
Extensions integrate via: plugin components (<PluginComponent pluginId='...' />), context providers, routes, and i18n namespaces.
Extension marking in code:
typescript
1/** @sfdc-extension-line SFDC_EXT_FEATURE_NAME */
2{
3 /* @sfdc-extension-block-start SFDC_EXT_FEATURE_NAME */
4}
5{
6 /* @sfdc-extension-block-end SFDC_EXT_FEATURE_NAME */
7}
8/** @sfdc-extension-file SFDC_EXT_FEATURE_NAME */
Commands
All commands run from frontend/:
bash
1# Development
2pnpm dev # Dev server at localhost:5173
3pnpm storybook # Storybook at localhost:6006
4
5# Build & Deploy
6pnpm build # Production build (+ cartridge generation)
7pnpm push # Deploy to Commerce Cloud MRT
8
9# Testing (use :agent variants for condensed output)
10pnpm test:agent # Unit tests (condensed, last 30 lines)
11pnpm test # Unit tests (verbose with coverage)
12pnpm test src/components/my-component # Single component tests
13pnpm test:watch src/components/my-comp # Watch mode
14pnpm test-storybook:interaction:agent # Interaction tests (condensed)
15pnpm test-storybook:a11y:agent # Accessibility tests (condensed)
16pnpm test-storybook:snapshot:agent # Snapshot tests (condensed)
17
18# Code Quality
19pnpm lint # ESLint
20pnpm lint:fix # Auto-fix lint issues
21pnpm typecheck # TypeScript checking
22pnpm lint:colors # Check for hard-coded colors
23
24# Code Generation
25pnpm generate:cartridge # Generate Page Designer metadata → backend/
Testing
Framework: Vitest 4 + React Testing Library + MSW for API mocking
typescript
1import { render, screen } from '@testing-library/react';
2import userEvent from '@testing-library/user-event';
3import { describe, it, expect } from 'vitest';
4import ProductCard from './index';
5
6describe('ProductCard', () => {
7 it('renders product name', () => {
8 render(<ProductCard product={mockProduct} />);
9 expect(screen.getByText('Test Product')).toBeInTheDocument();
10 });
11
12 it('handles click', async () => {
13 const user = userEvent.setup();
14 render(<ProductCard product={mockProduct} />);
15 await user.click(screen.getByRole('button'));
16 // assertions
17 });
18});
Key setup (vitest.setup.ts): Mocks window.__APP_CONFIG__, initializes i18n, stubs matchMedia, ResizeObserver, IntersectionObserver.
Coverage excludes: src/components/ui/**, stories, test files, mocks, snapshots.
Storybook Stories
typescript
1import type { Meta, StoryObj } from "@storybook/react-vite";
2import ProductCard from "../index";
3
4const meta: Meta<typeof ProductCard> = {
5 title: "Components/ProductCard",
6 component: ProductCard,
7 tags: ["autodocs"],
8};
9export default meta;
10type Story = StoryObj<typeof meta>;
11
12export const Default: Story = { args: { product: mockProduct } };
Adapter Pattern
The adapter pattern enables swappable data-fetching implementations (Einstein, Active Data, custom) without changing components.
Flow: Component → Hook → Provider → Adapter Registry → Concrete Adapter
Key files: src/lib/adapters/ (registry, types, implementations), src/providers/ (context providers), src/hooks/ (consumer hooks).
For full implementation details, read references/adapter-pattern.md.
Page Designer Integration
TypeScript decorators generate Commerce Cloud Page Designer metadata:
typescript
1import {
2 Component,
3 AttributeDefinition,
4 RegionDefinition,
5} from "@/lib/decorators";
6
7@Component("hero", { name: "Hero Banner", description: "Full-width hero" })
8@RegionDefinition([])
9export class HeroMetadata {
10 @AttributeDefinition() title?: string;
11 @AttributeDefinition({ type: "image" }) imageUrl?: string;
12}
Run pnpm generate:cartridge to output metadata to backend/cartridges/app_storefrontnext_base/.
For deep dive, read references/page-designer.md.
These patterns are especially important for this e-commerce app:
-
Parallel data fetching in loaders — Return multiple promises, let React Router stream:
typescript
1export function loader({ context }: LoaderFunctionArgs) {
2 return {
3 product: fetchProduct(context, id),
4 reviews: fetchReviews(context, id), // Fetched in parallel
5 };
6}
-
Minimize client component boundaries — Each "use client" boundary serializes its props. Keep the boundary as low as possible in the component tree.
-
Use Suspense for streaming — Wrap slow async components in <Suspense fallback={<Skeleton />}>.
-
Bundle size limits — Configured in package.json under bundlesize. Check with pnpm bundlesize:test.
-
Code splitting — Locales auto-split per language. Extensions load independently.
Common Pitfalls
| Mistake | Why it's bad | Fix |
|---|
async loader function | Blocks streaming, breaks React Router patterns | Return promises directly, don't await |
"use client" on everything | Bloats client bundle, loses RSC benefits | Default to server components |
clientLoader / clientAction | All fetching must be server-side | Use loader / action only |
| Hard-coded hex colors | Breaks theming, fails ESLint | Use Tailwind design tokens |
Relative imports ../../ | Fragile, hard to refactor | Use @/ path alias |
| Missing copyright header | Fails ESLint pre-commit hook | Add Apache 2.0 header |
Editing src/components/ui/ | These are shared primitives | Create feature component instead |
Importing Heart from lucide-react | Project has wrappers with consistent sizing/a11y | Use HeartIcon from @/components/icons/heart-icon |
| Raw strings in JSX | Can't localize | Use t('key') from react-i18next |
any types | Defeats type safety | Use proper types or generics |
| Manual Page Designer JSON | Gets overwritten by generation | Use TypeScript decorators |
When to Read Reference Files
For deeper patterns on specific topics, read these files in references/:
adapter-pattern.md — When implementing a new adapter, modifying the adapter registry, creating adapter providers/hooks, or working with Einstein/Active Data/Data Cloud integrations.
page-designer.md — When creating Page Designer components with decorators, understanding metadata generation, or working with the generate:cartridge pipeline.
design-to-decorators.md — When analyzing a visual design (Figma, screenshot, or verbal description) to determine Page Designer decorator configuration, attribute types, region planning, or component decomposition.
testing-patterns.md — When writing complex tests, setting up MSW API mocks, testing hooks/providers, or working with Storybook interaction tests.
This skill works alongside:
- vercel-react-best-practices — For general React performance optimization patterns (async waterfalls, bundle size, re-renders)
- vercel-composition-patterns — For component architecture patterns (compound components, avoiding boolean props, context interfaces)
- sfcc-backend — For backend/ cartridge code (SFRA, ISML, dw.* APIs)