dist/codex/shopify-commerce/skills/react-patterns/SKILL.md
Build React applications with Remix — loaders, actions, hooks, Server/Client Components, nested routes, error boundaries, form handling, and streaming SSR. Use when building Shopify Hydrogen storefronts or Remix-based apps.
npx skillsauth add orcaqubits/agentic-commerce-claude-plugins react-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Fetch live docs:
https://remix.run/docs/en/main for Remix documentationhttps://react.dev/reference/react for React API referencesite:shopify.dev hydrogen remix patterns for Hydrogen-specific patternsShopify's Hydrogen is built on Remix:
Server-side function that runs on every GET request:
import { json, type LoaderFunctionArgs } from '@remix-run/node';
export async function loader({ context, params }: LoaderFunctionArgs) {
const { storefront } = context;
const { products } = await storefront.query(PRODUCTS_QUERY);
return json({ products });
}
export default function ProductsPage() {
const { products } = useLoaderData<typeof loader>();
return <ProductGrid products={products} />;
}
Server-side function for form submissions (POST/PUT/DELETE):
import { redirect, type ActionFunctionArgs } from '@remix-run/node';
export async function action({ request, context }: ActionFunctionArgs) {
const formData = await request.formData();
const variantId = formData.get('variantId') as string;
const { cart } = context;
await cart.addLines([{ merchandiseId: variantId, quantity: 1 }]);
return redirect('/cart');
}
Routes compose via <Outlet>:
app/routes/
├── ($locale)._index.tsx # Homepage
├── ($locale).products._index.tsx # Product listing
├── ($locale).products.$handle.tsx # Product detail
├── ($locale).collections.$handle.tsx # Collection page
├── ($locale).cart.tsx # Cart page
└── ($locale).account.tsx # Account layout
├── ($locale).account._index.tsx # Account dashboard
└── ($locale).account.orders.tsx # Order history
Per-route error handling:
export function ErrorBoundary() {
const error = useRouteError();
if (isRouteErrorResponse(error)) {
return (
<div>
<h1>{error.status}</h1>
<p>{error.statusText}</p>
</div>
);
}
return <div>Something went wrong</div>;
}
| Hook | Purpose |
|------|---------|
| useState | Local component state |
| useEffect | Side effects (client only) |
| useRef | Mutable ref / DOM access |
| useMemo | Memoized computation |
| useCallback | Memoized callback |
| useContext | Context consumption |
| useReducer | Complex state logic |
| Hook | Purpose |
|------|---------|
| useLoaderData | Access loader data |
| useActionData | Access action response |
| useFetcher | Non-navigation data fetching |
| useNavigation | Navigation state (loading, submitting) |
| useRouteError | Error boundary data |
| useSearchParams | URL search parameters |
| useParams | Route parameters |
| useMatches | All matched routes data |
async/await directlyuseState, no useEffect"use client" directive (in React 19+)useState, useEffect, useRefRemix enhances HTML forms:
import { Form, useNavigation } from '@remix-run/react';
function AddToCartForm({ variantId }: { variantId: string }) {
const navigation = useNavigation();
const isAdding = navigation.state === 'submitting';
return (
<Form method="post" action="/cart">
<input type="hidden" name="variantId" value={variantId} />
<button type="submit" disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
</Form>
);
}
For mutations that shouldn't navigate:
function AddToCartButton({ variantId }: { variantId: string }) {
const fetcher = useFetcher();
const isAdding = fetcher.state === 'submitting';
return (
<fetcher.Form method="post" action="/cart">
<input type="hidden" name="variantId" value={variantId} />
<button disabled={isAdding}>
{isAdding ? 'Adding...' : 'Add to Cart'}
</button>
</fetcher.Form>
);
}
Defer non-critical data for faster initial render:
import { defer } from '@remix-run/node';
import { Await, useLoaderData } from '@remix-run/react';
import { Suspense } from 'react';
export async function loader({ context }: LoaderFunctionArgs) {
const criticalData = await context.storefront.query(PRODUCT_QUERY);
const recommendedProducts = context.storefront.query(RECOMMENDATIONS_QUERY);
return defer({
product: criticalData.product,
recommended: recommendedProducts, // not awaited — streams later
});
}
export default function ProductPage() {
const { product, recommended } = useLoaderData<typeof loader>();
return (
<div>
<ProductDetail product={product} />
<Suspense fallback={<Spinner />}>
<Await resolve={recommended}>
{(data) => <RecommendedProducts products={data.products} />}
</Await>
</Suspense>
</div>
);
}
function ProductCard({ product, children }: { product: Product; children?: ReactNode }) {
return (
<article>
<ProductImage image={product.featuredImage} />
<ProductTitle title={product.title} />
<ProductPrice price={product.priceRange} />
{children}
</article>
);
}
function useCart() {
const fetcher = useFetcher();
const addToCart = (variantId: string) => {
fetcher.submit({ variantId }, { method: 'post', action: '/cart' });
};
return { addToCart, isAdding: fetcher.state === 'submitting' };
}
useEffect<Form> over manual fetchuseFetcher for mutations that should not cause navigationdefer() + <Suspense> for non-critical data (recommendations, reviews)useEffect for data fetching — Remix loaders handle thisFetch the Remix and React documentation for exact API signatures, hook behavior, and streaming patterns before implementing.
development
Build with Spree's headless Next.js storefront — the official `spree/storefront` repo (Next.js 16 App Router with Server Actions and Turbopack, React 19 Server Components, Tailwind CSS 4, TypeScript 5, `@spree/sdk`, Sentry), server-only auth (httpOnly JWT cookies + publishable key), MeiliSearch faceted catalog, one-page checkout with Apple/Google Pay/Klarna/Affirm/SEPA, multi-region market routing, GA4 + JSON-LD SEO, and Vercel/Docker deployment. Use when forking or customizing the storefront, or evaluating headless adoption.
tools
Build Spree extensions as Rails engines — gem scaffolding, `bin/rails g spree:extension`, mounting routes/migrations/assets, the modern `prepend` decorator pattern (`*_decorator.rb` with `self.prepended(base)`), generators (`spree:model_decorator`, `spree:controller_decorator`), the four customization surfaces in preference order (Events > Webhooks > Dependencies > Decorators), Spree::Dependencies for swapping service objects, gem release/versioning, and the deprecated Deface engine. Use when building a reusable Spree extension or adding non-trivial customization to an app.
development
Build with Spree's event bus and Webhooks 2.0 — `Spree::Events` publication, `Spree::Subscriber` DSL with `subscribes_to` and `on`, wildcard matching, lifecycle events (`{model}.created/.updated/.deleted` via `publishes_lifecycle_events`), the canonical event catalog (order.*, payment.*, shipment.*, product.*), Webhooks 2.0 endpoints, HMAC-SHA256 signing (`X-Spree-Webhook-Signature`), exponential-backoff retries, and Sidekiq job orchestration. Use when wiring event-driven business logic, building webhook consumers, or replacing ActiveSupport callback chains.
tools
Cross-cutting Spree development patterns — the customization preference hierarchy (Events > Webhooks > Dependencies > Decorators), `Spree::Dependencies` service-object swapping, the `_decorator.rb` + `prepend` + `self.prepended` idiom, idempotent subscribers and webhook receivers, multi-store scoping discipline, prefixed IDs, calculator polymorphism (shipping/promotion/tax share the base), service-object composition with `dry-monads` or simple results, why to avoid `class_eval` reopening and Deface, and Spree-on-Rails idioms (Hotwire/Turbo Stimulus, ActiveStorage, Action Cable, Sidekiq). Use when designing the architecture of a Spree extension or solving cross-cutting concerns.