from schema to finished product page
A React storefront built on Magento's GraphQL API fully decouples frontend and backend, making the shop faster, more flexible and easier to maintain. Teams that understand the schema, pick the right client and plan caching and error handling from day one save months of later rework.
Table of Contents
- 1. Why a React storefront on Magento GraphQL
- 2. Understanding the Magento GraphQL schema
- 3. Configuring and setting up Apollo Client
- 4. Loading product listings, facets and pagination
- 5. Product detail pages with configurable products
- 6. Customer token and session handling
- 7. Caching strategy for the React storefront
- 8. Handling Magento GraphQL error responses
- 9. React storefront approaches compared
- 10. Summary
- 11. FAQ
1. Why a React storefront on Magento GraphQL
A React storefront built on Magento's GraphQL API is the logical evolution beyond the classic Luma frontend. Instead of server-rendered phtml templates, Magento only ships data, and React takes over rendering, routing and interactivity entirely in the browser. This approach decouples the release cycles of frontend and backend, allows a dedicated deployment pipeline for the storefront, and opens the door to modern tooling like Vite, TanStack Query and Tailwind, none of which ever integrated cleanly into the Luma context.
The key difference from PWA Studio or a generic headless setup is that a hand-built React storefront is tailored exactly to a specific shop's requirements. No unused code, no foreign dependencies, no compromises on bundle size. Anyone who has tried to adapt a PWA Studio theme to a custom design system knows the effort required to strip out unwanted components. A custom React storefront starts at zero and only grows by what is actually needed.
Technically, every React storefront on Magento GraphQL rests on three pillars: the GraphQL schema as the contract between backend and frontend, a client such as Apollo or urql for caching and request management, and a clean separation between server and client state. The following sections walk through each pillar in detail, with runnable examples for product listings, product detail pages and authentication.
2. Understanding the Magento GraphQL schema
Before writing a single line of React code, it pays to take a thorough look at the Magento GraphQL schema through the built-in GraphiQL interface at /graphql. The schema follows the Relay connection specification for paginated lists: fields like products do not return a plain array but a structure with items, page_info and total_count. A React storefront that ignores this convention ends up with pagination logic that breaks on categories holding thousands of products.
A second important building block is the distinction between ProductInterface and the concrete types SimpleProduct, ConfigurableProduct and BundleProduct. GraphQL queries must use inline fragments (... on ConfigurableProduct) to request type-specific fields such as configurable_options. For a React storefront that renders multiple product types in the same catalog, this pattern is unavoidable and should be centralized early in shared fragment definitions.
# GraphQL query fragment for a Magento product tile in a React storefront
fragment ProductTileFields on ProductInterface {
uid
name
sku
url_key
small_image {
url
label
}
price_range {
minimum_price {
final_price { value currency }
regular_price { value currency }
}
}
... on ConfigurableProduct {
configurable_options {
attribute_code
label
values { value_index label }
}
}
}
query CategoryProducts($categoryId: String!, $pageSize: Int!, $currentPage: Int!) {
products(filter: { category_id: { eq: $categoryId } }, pageSize: $pageSize, currentPage: $currentPage) {
total_count
page_info { current_page page_size total_pages }
items { ...ProductTileFields }
}
}
The benefit of this fragment-based structure becomes clear when scaling the React storefront: new pages that render the same product tiles simply import the existing fragment instead of listing fields again. That significantly reduces inconsistencies between the category page, search results and cross-sell widgets, because all three request exactly the same fields and therefore share the same Apollo cache entry.
3. Configuring and setting up Apollo Client
For a production-grade React storefront, Apollo Client has become the standard GraphQL client because it provides normalized caching, optimistic updates and a mature DevTools integration. The alternative, urql, is lighter and quicker to set up, but offers a less granular cache model for complex product data with nested pricing structures, as they occur with bundle and configurable products in Magento.
When setting up Apollo Client for a React storefront, the correct configuration of the InMemoryCache is critical. Magento GraphQL returns a uid for most entities that serves as a unique cache key. Without explicit typePolicies, Apollo automatically normalizes product data via id, but Magento consistently uses uid, so a custom key function is required for cache updates to work correctly after mutations such as adding an item to the cart.
// apolloClient.js — Apollo Client setup for a React storefront on Magento GraphQL
import { ApolloClient, InMemoryCache, createHttpLink, from } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { onError } from '@apollo/client/link/error';
const httpLink = createHttpLink({ uri: process.env.NEXT_PUBLIC_MAGENTO_GRAPHQL_URL });
// Attach store and customer context headers on every request
const authLink = setContext((_, { headers }) => {
const token = typeof window !== 'undefined' ? localStorage.getItem('customerToken') : null;
return {
headers: {
...headers,
Store: process.env.NEXT_PUBLIC_MAGENTO_STORE_CODE,
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
};
});
const errorLink = onError(({ graphQLErrors, networkError }) => {
graphQLErrors?.forEach((err) => console.error('[GraphQL error]', err.message, err.extensions));
if (networkError) console.error('[Network error]', networkError);
});
export const apolloClient = new ApolloClient({
link: from([errorLink, authLink, httpLink]),
cache: new InMemoryCache({
typePolicies: {
ProductInterface: { keyFields: ['uid'] },
ConfigurableProduct: { keyFields: ['uid'] },
Cart: { keyFields: ['id'] },
},
}),
});
A detail that is often underestimated in React storefront projects is the Store header. Magento uses it in multi-store setups to determine the correct store view and therefore prices, language and availability. Without the header, the default store view is used, which leads to wrong prices or missing translations in international shops, a bug that often only surfaces in staging with a second store.
4. Loading product listings, facets and pagination
Category pages are where a React storefront shows its performance advantage over Luma most clearly. Instead of a full page reload on every filter click, React only updates the affected components while Apollo Client fires the request in the background and caches the result in normalized form. The aggregations in the GraphQL response deliver facet data (size, color, price range) including hit counts per option, which can be used directly for filter badges on the client.
For pagination, a React storefront benefits from the cursor-like pattern with currentPage and pageSize, combined with Apollo's fetchMore for infinite-scroll variants or classic page navigation. It is important to mirror the filter state in the URL so filtered category pages remain shareable and crawlable by search engines, a point that is easily overlooked with purely client-side state.
// CategoryProductGrid.jsx — filterable product grid for a React storefront
import { useQuery } from '@apollo/client';
import { useSearchParams } from 'next/navigation';
import { CATEGORY_PRODUCTS_QUERY } from './queries';
export function CategoryProductGrid({ categoryId }) {
const searchParams = useSearchParams();
const page = Number(searchParams.get('p') ?? '1');
const priceFilter = searchParams.get('price');
const { data, loading, fetchMore } = useQuery(CATEGORY_PRODUCTS_QUERY, {
variables: {
categoryId,
pageSize: 24,
currentPage: page,
filter: priceFilter ? { price: { from: priceFilter.split('-')[0], to: priceFilter.split('-')[1] } } : {},
},
notifyOnNetworkStatusChange: true,
});
if (loading && !data) return <ProductGridSkeleton count={24} />;
const { items, total_count, page_info } = data.products;
return (
<>
<FacetSidebar aggregations={data.products.aggregations} />
<ProductTiles items={items} />
<Pagination current={page_info.current_page} total={page_info.total_pages} totalCount={total_count} />
</>
);
}
5. Product detail pages with configurable products
The product detail page is the most complex single building block of any React storefront, because configurable products require multi-dimensional variant logic. Magento GraphQL provides the available attributes (say, size and color) via configurable_options with all possible values, but it does not directly state which combinations are actually in stock. That information lives in variants, an array of concrete product variants with their attribute combination and SKU.
A robust selection pattern for the React storefront builds a lookup table from variants that finds the matching variant in fractions of a second based on the selected attribute values, including stock status and variant-specific image. Without this preprocessing, every click on a color option would require a fresh GraphQL request, which feels noticeably sluggish and generates unnecessary server load.
// useConfigurableSelection.js — variant lookup for React storefront PDPs
import { useMemo, useState } from 'react';
export function useConfigurableSelection(configurableProduct) {
const [selected, setSelected] = useState({});
// Build a lookup map: "64_red" -> variant
const variantMap = useMemo(() => {
const map = new Map();
configurableProduct.variants.forEach((variant) => {
const key = variant.attributes
.map((a) => `${a.code}_${a.value_index}`)
.sort()
.join('|');
map.set(key, variant);
});
return map;
}, [configurableProduct.variants]);
const activeVariant = useMemo(() => {
const key = Object.entries(selected)
.map(([code, value]) => `${code}_${value}`)
.sort()
.join('|');
return variantMap.get(key) ?? null;
}, [selected, variantMap]);
const selectOption = (code, valueIndex) =>
setSelected((prev) => ({ ...prev, [code]: valueIndex }));
return { selected, selectOption, activeVariant, inStock: activeVariant?.product.stock_status === 'IN_STOCK' };
}
6. Customer token and session handling
Authentication is one area where a hand-built React storefront requires noticeably more care than a server-rendering approach. Magento GraphQL uses bearer token authentication via the generateCustomerToken mutation, which returns a token with limited validity. That token must be stored securely, where localStorage is convenient but vulnerable to XSS, while HttpOnly cookies routed through a dedicated backend-for-frontend layer offer more protection but require additional infrastructure.
For guest checkouts, a React storefront additionally works with a cart_id that exists independently of the customer token and is persisted in a cookie or localStorage. The crucial step when transitioning from guest to logged-in customer is the mergeCarts mutation, which merges the anonymous cart with the customer's cart, a step that is forgotten in many implementations and leads to lost carts after login.
7. Caching strategy for the React storefront
Caching largely determines the perceived speed of a React storefront. Static data such as store configuration, category tree and CMS blocks change rarely and are ideal candidates for server-side caching with a long time to live, combined with explicit invalidation via Magento's cache tags, provided the deployment platform supports incremental static regeneration. Product prices and stock levels, on the other hand, are more volatile and should be handled client-side with a short Apollo cache time to live or a network-only fetch policy.
A proven pattern for the React storefront combines cache-first for navigation data with cache-and-network for product listings: the user immediately sees the cached state while updated prices and availability load in the background. That avoids visible loading states on repeat visits to the same category without displaying stale prices once the fresh response arrives.
8. Handling Magento GraphQL error responses
Magento GraphQL returns errors on two levels: as an HTTP status code for transport problems and as an errors array inside an HTTP 200 response for business errors such as invalid SKUs or expired carts. A React storefront that only checks the HTTP status misses these business errors entirely, because GraphQL responds with status 200 even when a query partially fails.
Every Magento GraphQL error carries a category in extensions.category, such as graphql-no-such-entity or graphql-authorization, which can be used for targeted error handling in the React storefront: expired tokens trigger an automatic re-login flow, missing entities show a 404 page, and validation errors are displayed directly next to the affected form field instead of a generic message at the edge of the page.
// errorCategoryHandler.js — routing Magento GraphQL errors by category in a React storefront
export function handleGraphQlError(error, { onAuthExpired, onNotFound, onValidation }) {
const category = error.extensions?.category;
switch (category) {
case 'graphql-authorization':
return onAuthExpired();
case 'graphql-no-such-entity':
return onNotFound();
case 'graphql-input':
return onValidation(error.message);
default:
console.error('[Unhandled GraphQL error]', error.message, category);
}
}
9. React storefront approaches compared
There are several ways to process GraphQL data in a React storefront. The choice significantly affects caching behavior, bundle size and development speed.
| Approach | Caching | Bundle size | Recommendation |
|---|---|---|---|
| Apollo Client | Normalized, granular | ~35 KB gzip | Complex catalogs, many mutations |
| urql | Document cache | ~9 KB gzip | Smaller storefronts, simple queries |
| fetch + TanStack Query | Query-key based | ~13 KB gzip | Team already knows REST patterns |
| PWA Studio (Peregrine) | Fixed, hard to customize | Large, lots of overhead | Only under a very tight timeline |
For most medium to large catalogs, Apollo Client is the most solid foundation for a React storefront, because normalized caching automatically produces consistent UI updates across multiple components after cart mutations, without manual re-fetching. Smaller projects with a modest query surface often move faster with urql, because the barrier to entry is lower and configuration overhead is reduced.
Mironsoft
React storefronts on Magento GraphQL, from architecture to deployment
Want your own React storefront for your Magento shop?
We build React storefronts on top of Magento GraphQL, with Apollo Client, clean caching and robust error handling, tailored to your catalog and your design.
Architecture consulting
Schema analysis, client selection and data model for your storefront
Implementation
Product pages, cart and checkout built on Magento GraphQL
Performance tuning
Caching strategy, bundle analysis and Core Web Vitals optimization
10. Summary
A React storefront on Magento GraphQL rests on three pillars: a well-understood schema with fragments for reusability, a client such as Apollo that cleanly manages normalized caching and mutations, and an explicit strategy for authentication, caching and error handling. Teams that think through these three areas from the start avoid the typical pitfalls with configurable products, multi-store setups and guest-to-customer transitions.
The biggest advantage over PWA Studio lies in full control over bundle size and dependencies. A React storefront that only queries the GraphQL fields it actually needs and only bundles the components it actually uses loads noticeably faster than a generic headless framework with unused overhead. That control pays off long term in better Core Web Vitals and lower bounce rates.
React Storefront on Magento GraphQL — Key Takeaways
Schema
Relay connections for lists, inline fragments for product types. Centralize fragment definitions for consistency.
Client
Apollo Client with keyFields: ['uid'] for correct cache updates after cart mutations.
Auth & cart
Bearer token for customers, cart_id for guests, don't forget mergeCarts on login.
Error handling
Inspect extensions.category on GraphQL errors, don't rely on HTTP status alone.