fynd-theme
Fynd FDK React Theme Skill
Production-grade React e-commerce theme tightly coupled to the Fynd Commerce platform. All logic for payments, checkout, cart, shipping, delivery, OMS, post-order journey, and authentication is already implemented in the theme's hooks, sections, and pages. Do not re-implement existing platform modules — extend or modify them.
Execution Visibility
When this skill is used, begin the response with: Executing the fynd-theme skill.
Platform Context
This is not a standalone React app. It runs inside the Fynd platform runtime, which handles:
- Routing and page rendering (SSR + SPA hydration)
- All e-commerce modules — cart, checkout, payments, OMS, auth — already wired via FDK hooks
- Theme configuration surfaced in the Fynd Admin Panel via
settings_schema.json - Asset delivery and deployment via FDK-CLI (
fdk theme serve,fdk theme sync)
Three content layers:
| Layer | Path | Role |
|---|---|---|
| Pages | theme/pages/ |
Full route-level components (47 pages) |
| Sections | theme/sections/ |
Reusable content blocks (60 sections); primary customization units merchants drag-and-drop |
| Components | theme/components/ |
Primitive UI components (49) used by pages and sections |
Rule Categories by Priority
| Priority | Category | Impact |
|---|---|---|
| 1 | SSR Compatibility | CRITICAL |
| 2 | FPI Data Fetching | CRITICAL |
| 3 | Platform Boundaries | CRITICAL |
| 4 | URL & Navigation | HIGH |
| 5 | Analytics & Events | HIGH |
| 6 | Image Optimization | HIGH |
| 7 | State & Re-render Optimization | MEDIUM |
| 8 | Styling Conventions | MEDIUM |
| 9 | Component Design | MEDIUM |
| 10 | Theme Configuration | MEDIUM |
| 11 | Bundle & Performance | MEDIUM |
1. SSR Compatibility (CRITICAL)
All code executes on the server during initial page render. Any access to browser globals at module or component initialization time will crash the server render.
Never access at module scope or component body:
// BAD — crashes server render
const isMobile = window.innerWidth < 768;
function MyComponent() {
const token = localStorage.getItem("auth_token"); // crashes on server
return <div />;
}
Always guard or move to effects/handlers:
// GOOD — guarded inline
const width = typeof window !== "undefined" ? window.innerWidth : 0;
// GOOD — effect is browser-only
function MyComponent() {
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
setIsMobile(window.innerWidth < 768);
const token = localStorage.getItem("auth_token");
}, []);
}
Rules:
- Never use
window,document,navigator,locationoutside effects or event handlers - Never use
localStorage/sessionStorage/IntersectionObserver/ResizeObserveroutsideuseEffect - Event handlers are browser-only by nature — safe to use browser APIs inside them
Every page needs two data fetch paths:
// SSR path — in pageDataResolver (theme/helper/lib.js), runs on server
// SPA path — in useEffect, runs when navigating client-side
useEffect(() => {
if (!productData) {
fpi.catalog.getProduct({ slug });
}
}, [slug]);
Both must exist. The pageDataResolver handles SSR; useEffect handles SPA navigation.
Deep dive:
fynd-theme/rules/ssr-browser-guards.md
2. FPI Data Fetching (CRITICAL)
All platform data is fetched through the FPI GraphQL client (fdk-store). Never use raw fetch or axios to call Fynd APIs.
Accessing the client:
import { useFPI, useGlobalStore } from "fdk-core/utils";
function ProductCard({ slug }) {
const fpi = useFPI();
const product = useGlobalStore(fpi.getters.PRODUCT_DETAILS);
const cartItems = useGlobalStore(fpi.getters.CART_ITEMS);
}
Parallel dispatch for independent fetches — never sequential:
// BAD — 3× latency waterfall
await fpi.catalog.getProduct({ slug });
await fpi.catalog.getSimilarProducts({ slug });
await fpi.catalog.getProductReviews({ slug });
// GOOD — single round-trip
await Promise.all([
fpi.catalog.getProduct({ slug }),
fpi.catalog.getSimilarProducts({ slug }),
fpi.catalog.getProductReviews({ slug }),
]);
Get only what you need in a single call. Avoid over-fetching.
Defer store reads to usage point — avoid subscribing to values only needed in callbacks:
// BAD — subscribes globally, re-renders on every cart update
const cart = useGlobalStore(fpi.getters.CART);
function handleAdd() { if (!cart.loading) fpi.cart.addItems(...); }
// GOOD — reads on demand, no subscription
function handleAdd() {
const cart = fpi.store.getState(fpi.getters.CART);
if (!cart.loading) fpi.cart.addItems(...);
}
SWR caching: When ENABLE_SWR_CACHE is set in theme settings, use theme/helper/fpi-swr-wrapper.js. Do not build a separate cache layer.
Data resolver responsibilities:
| Resolver | File | When | Purpose |
|---|---|---|---|
globalDataResolver |
theme/helper/lib.js |
Once on app init (SSR) | Global config, location, user session |
pageDataResolver |
theme/helper/lib.js |
Per page (SSR) | Page-specific initial data |
Keep both lean — they block the SSR render. Defer non-critical data to useEffect.
Deep dive:
fynd-theme/rules/fpi-data-fetching.md
3. Platform Boundaries — Don't Reimplement Built-in Modules (CRITICAL)
The platform provides complete implementations of every core e-commerce module. Do not recreate them.
Modules already implemented — do not recreate:
| Module | Handled by |
|---|---|
| Authentication (login, register, OTP, social) | FPI store + theme/helper/auth-guard.js |
| Cart (add, remove, update, coupons) | fpi.cart.* actions |
| Checkout (address, delivery, payment) | theme/sections/checkout.jsx + FPI actions |
| Payments | FPI payment actions + platform PG integration |
| Order placement & OMS | fpi.order.* actions |
| Post-order (returns, cancellations, refunds) | FPI order management actions |
| Shipping / delivery promise | FPI logistics actions |
| Wishlist | fpi.catalog.wishlist.* actions |
| Product catalog, search, filters | fpi.catalog.* actions |
| User profile | fpi.user.* actions |
Incorrect — parallel custom cart state:
// Creates conflicting state — diverges from platform cart
const [cartItems, setCartItems] = useState([]);
function addToCart(product) {
setCartItems((prev) => [...prev, product]);
fetch("/api/cart/add", { body: JSON.stringify(product) }); // bypasses FPI
}
Correct — use FPI:
const fpi = useFPI();
const cartItems = useGlobalStore(fpi.getters.CART_ITEMS);
async function addToCart(product, size) {
await fpi.cart.addItems({
items: [{ item_id: product.uid, item_size: size, quantity: 1 }],
});
// store auto-updates — no manual setState
}
Before modifying any hook in theme/helper/hooks.jsx or section in theme/sections/, read the full file. These hooks manage analytics dispatch, platform state sync, error handling, and production edge cases. Partial rewrites break these invariants.
Deep dive:
fynd-theme/rules/platform-boundaries.md
4. URL & Navigation — Never Hardcode Storefront URLs (HIGH)
The platform appends UTM parameters and manages multi-tenant routing. Hardcoded paths strip attribution and break across deployments.
Incorrect:
router.push("/products/red-shoes"); // strips UTM
window.location.href = "/cart"; // bypasses platform router
<a href="/collection/sale">Shop Sale</a> // breaks multi-tenant
Correct — action objects:
import { useNavigate } from "fdk-core/utils";
import { FDKLink } from "fdk-core/components";
const navigate = useNavigate();
navigate({ action: "product", params: { slug: product.slug } });
<FDKLink action="collection" params={{ slug: "sale" }}>Shop Sale</FDKLink>
Common actions:
| Page | Action |
|---|---|
| Home | { action: "home" } |
| PDP | { action: "product", params: { slug } } |
| Collection / PLP | { action: "collection", params: { slug } } |
| Cart | { action: "cart" } |
| Checkout | { action: "checkout" } |
| Order detail | { action: "order-detail", params: { orderId } } |
| Profile | { action: "profile" } |
| Search | { action: "search", params: { query } } |
Never hardcode domain names. For full URLs: fpi.router.getPageURL({ action, params }).
Deep dive:
fynd-theme/rules/url-navigation.md
5. Analytics & FPI Events (HIGH)
FPI analytics events feed merchant dashboards, GTM, and attribution. Incorrect events corrupt reporting.
Fire events only at their intended trigger point:
| Event | Correct Trigger |
|---|---|
product_viewed |
When PDP data first loads |
add_to_cart |
After successful fpi.cart.addItems() response |
checkout_started |
When user lands on checkout page |
purchase |
After successful order confirmation |
product_list_viewed |
When PLP renders with its product list |
Never fire events in effects that can re-run:
// BAD — fires on every re-render
useEffect(() => {
fpi.analytics.fireEvent("product_viewed", { product });
});
// GOOD — fires exactly once when product data loads
const hasTracked = useRef(false);
useEffect(() => {
if (product && !hasTracked.current) {
hasTracked.current = true;
fpi.analytics.fireEvent("product_viewed", { product });
}
}, [product]);
Never fire events during SSR — analytics require browser context (cookies, GTM).
Don't push to window.dataLayer directly — use fpi.analytics.fireEvent().
Before adding a new event call, check if the existing hook already fires it.
Deep dive:
fynd-theme/rules/analytics-events.md
6. Image Optimization (HIGH)
Always resize images through Pixelbin via transformImage. Never use raw CDN URLs.
import { transformImage } from "../../helper/utils";
// Size to actual rendered dimensions
<img
src={transformImage({ src: product.media[0].url, width: 400, height: 400, format: "webp" })}
width={400}
height={400}
loading="lazy"
alt={product.name}
/>
LCP images (hero, first product card) — eager + high priority:
<img
src={transformImage({ src, width: 800, format: "webp" })}
loading="eager"
fetchpriority="high"
alt={alt}
/>
Rules:
- Always set
width/heightattributes — prevents layout shift (CLS) - Request images at their rendered size — not original resolution
loading="lazy"for all below-fold images- Use
srcSetfor components that render at different sizes across breakpoints
Deep dive:
fynd-theme/rules/image-optimization.md
7. State & Re-render Optimization (MEDIUM)
Hoist non-primitive default props outside components:
// BAD — new object reference every render, breaks memo
<Component style={{ padding: 8 }} />
// GOOD
const DEFAULT_STYLE = { padding: 8 };
<Component style={DEFAULT_STYLE} />
Use functional setState when new state derives from previous:
setCount((prev) => prev + 1); // not setCount(count + 1)
Use useRef for transient values that don't need re-renders:
Scroll position, timer IDs, animation frame IDs, "has tracked" flags.
Use React.memo on list items:
Product cards, order rows, review items — any component rendered in a .map().
Prefer startTransition for non-urgent updates:
Search filtering, tab switching, sort changes.
Derive state during render instead of syncing with useEffect:
// BAD — effect + state for derived value
const [isDiscounted, setIsDiscounted] = useState(false);
useEffect(() => { setIsDiscounted(price < originalPrice); }, [price]);
// GOOD — compute directly
const isDiscounted = price < originalPrice;
Relevant Vercel rules: rerender-memo.md, rerender-memo-with-default-value.md, rerender-functional-setstate.md, rerender-use-ref-transient-values.md, rerender-derived-state-no-effect.md, rerender-transitions.md
8. Styling Conventions (MEDIUM)
- Less modules (
.module.less) for all component-scoped styles — nostyled-components,emotion, or Tailwind - CSS variables for merchant-configurable values (colors, fonts, spacing) — set by
ThemeProvider, consumed in Less theme/styles/base.global.lessfor global base styles only (resets,:rootvariable declarations, third-party overrides)- Mobile-first responsive styles via
@media (min-width: ...)breakpoints - Inline
styleprops only for truly dynamic JS-computed values (cursor position, measured heights) — not for layout/design
// GOOD — Less module with CSS variables
.button {
background-color: var(--button-primary-bg);
color: var(--button-primary-text);
font-family: var(--font-body);
@media (min-width: 768px) {
padding: 12px 24px;
}
}
Deep dive:
fynd-theme/rules/styling-conventions.md
9. Component Design (MEDIUM)
Check before creating — these already exist:
| Need | Where to look |
|---|---|
| Custom hooks | theme/helper/hooks.jsx — useThemeConfig, useAddress, usePolling, etc. |
| Utilities | theme/helper/utils.js — image sizing, sanitizeHTML, color utils |
| UI primitives | theme/components/ — carousel, modal, loader, form fields, rating, breadcrumb |
| Data resolvers | theme/helper/lib.js |
Keep components under ~300 lines. Split by responsibility when they grow.
Don't prop-drill beyond 2 levels. Use useGlobalStore or context.
Sanitize all platform HTML — product descriptions, CMS content, banners:
import { sanitizeHTML } from "../../helper/utils";
import parse from "html-react-parser";
// With DOMPurify sanitization (never skip this)
<div>{parse(sanitizeHTML(product.description))}</div>
// Never:
<div dangerouslySetInnerHTML={{ __html: product.description }} />
Hoist static JSX outside component body:
// BAD — new reference every render
function Icon() { return <svg>...</svg>; }
function Button() { return <button><Icon /></button>; }
// GOOD — created once
const ICON = <svg>...</svg>;
function Button() { return <button>{ICON}</button>; }
Sections vs Components:
- Sections — connected to FPI store, declare their own
propsschema, work standalone - Components — presentational, receive data via props, reusable across sections
Deep dive:
fynd-theme/rules/component-design.md
10. Theme Configuration (MEDIUM)
Any design decision a merchant should control must be in settings_schema.json — never hardcoded.
Read config via useThemeConfig() hook:
import { useThemeConfig } from "../../helper/hooks";
function MyComponent() {
const { global_config, page_config } = useThemeConfig();
const primaryFont = global_config?.custom?.props?.font_body;
const showReviews = page_config?.props?.show_reviews?.value;
}
Section props are received directly:
function HeroBanner({ props, globalConfig }) {
const { heading, background_color, cta_text } = props;
return <div style={{ backgroundColor: background_color?.value }}>{heading?.value}</div>;
}
Schema types: font, range, select, checkbox, text, url, image_picker, color, extension
Use CSS variables for runtime-configurable values — set by ThemeProvider on :root, consumed in Less. Never inline dynamic JS config values as style props.
Deep dive:
fynd-theme/rules/theme-configuration.md
11. Bundle & Performance (MEDIUM)
Every page import in theme/index.jsx must be a dynamic import with webpackChunkName:
// GOOD — code split per page
getProductDescription: () =>
import(/* webpackChunkName: "product-description" */ "./pages/product-description"),
Import from specific module paths, not barrel index files:
// BAD — may load entire library
import { formatPrice } from "../../helper";
// GOOD
import { formatPrice } from "../../helper/utils";
Defer third-party scripts after hydration:
useEffect(() => {
// Copilot, Google Maps, analytics — load after first render
import("./copilot").then((m) => m.init());
}, []);
For scroll-heavy pages (PLP, order history) consider content-visibility: auto in Less for off-screen sections.
Relevant Vercel rules: bundle-barrel-imports.md, bundle-dynamic-imports.md, bundle-defer-third-party.md, rendering-content-visibility.md
Not Applicable to This Codebase
frontend-design skill — for building new UIs with bold aesthetic choices. This codebase has an established design system (Less modules, CSS variables, settings_schema.json). Do not apply those aesthetic guidelines here.
Vercel server-* rules (RSC, React.cache(), after()) — these are Next.js App Router patterns. This project uses a custom Webpack + FDK SSR runtime. Use FPI data resolvers for server-side patterns instead.
Key File Reference
| File | Purpose |
|---|---|
theme/index.jsx |
Entry point — initializes FPI, exports all page/section/component references |
theme/global-provider.jsx |
ThemeProvider — wraps app, SEO meta, Copilot.live init |
theme/helper/utils.js |
transformImage, sanitizeHTML, color utils (33KB) |
theme/helper/hooks.jsx |
useThemeConfig, useAddress, usePolling, all custom hooks |
theme/helper/fpi-swr-wrapper.js |
SWR caching layer for FPI GraphQL responses |
theme/helper/lib.js |
globalDataResolver, pageDataResolver |
theme/helper/auth-guard.js |
Auth guard utilities |
settings_schema.json |
All merchant-configurable theme props |
webpack.config.js |
Build config — Less modules, code splitting, polyfills |
copilot/index.js |
Copilot.live AI integration entry |
Extended Rule Files (Deep Dives)
For more detailed explanations with additional code examples on each topic:
| Fynd Rule File | Topic |
|---|---|
fynd-theme/rules/ssr-browser-guards.md |
SSR safety patterns |
fynd-theme/rules/fpi-data-fetching.md |
FPI GraphQL patterns |
fynd-theme/rules/platform-boundaries.md |
Module ownership boundaries |
fynd-theme/rules/url-navigation.md |
Action-based navigation |
fynd-theme/rules/analytics-events.md |
Event dispatch safety |
fynd-theme/rules/image-optimization.md |
Pixelbin/transformImage usage |
fynd-theme/rules/theme-configuration.md |
settings_schema patterns |
fynd-theme/rules/styling-conventions.md |
Less modules + CSS variables |
fynd-theme/rules/component-design.md |
Component patterns |
Vercel React rules (generic React/JS performance): vercel-react-best-practices/rules/
Most relevant: async-parallel.md, bundle-barrel-imports.md, bundle-dynamic-imports.md, rerender-memo.md, rerender-derived-state-no-effect.md, rendering-hoist-jsx.md, rendering-conditional-render.md