From shopify-commerce
Provides Remix patterns for React apps: loaders/actions, nested routes, error boundaries, form handling, streaming SSR. For Shopify Hydrogen storefronts or Remix apps.
npx claudepluginhub orcaqubits/agentic-commerce-skills-plugins --plugin shopify-commerceThis skill is limited to using the following tools:
**Fetch live docs**:
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Searches prompts.chat for AI prompt templates by keyword or category, retrieves by ID with variable handling, and improves prompts via AI. Use for discovering or enhancing prompts.
Guides MCP server integration in Claude Code plugins via .mcp.json or plugin.json configs for stdio, SSE, HTTP types, enabling external services as tools.
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.