React Storefront
Project Setup
Next.js + Magento
# Create Next.js project
npx create-next-app magento-storefront
cd magento-storefront
# Install dependencies
npm install @apollo/client graphql
npm install @magento/venia-ui # Optional: use Venia components
GraphQL Client Configuration
// lib/apollo-client.ts
import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
const client = new ApolloClient({
link: new HttpLink({
uri: process.env.NEXT_PUBLIC_MAGENTO_GRAPHQL,
headers: {
'Store': 'default'
}
}),
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: {
fetchPolicy: 'cache-and-network',
},
},
});
export default client;
Product Listing Component
// components/ProductList.tsx
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCTS = gql`
query GetProducts($pageSize: Int!) {
products(filter: {}, pageSize: $pageSize) {
items {
id
name
sku
url_key
price_range {
minimum_price {
regular_price { value currency }
}
}
image {
url
}
}
}
}
`;
export default function ProductList({ pageSize = 20 }) {
const { loading, error, data } = useQuery(GET_PRODUCTS, {
variables: { pageSize }
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
return (
<div className="grid grid-cols-4 gap-4">
{data.products.items.map((product: any) => (
<div key={product.id} className="border rounded p-4">
<img src={product.image.url} alt={product.name} />
<h3>{product.name}</h3>
<p>${product.price_range.minimum_price.regular_price.value}</p>
</div>
))}
</div>
);
}
Product Detail Page
// pages/product/[sku].tsx
import { useRouter } from 'next/router';
import { useQuery, gql } from '@apollo/client';
const GET_PRODUCT = gql`
query GetProduct($sku: String!) {
products(filter: { sku: { eq: $sku } }) {
items {
name
sku
description { html }
price_range {
minimum_price {
regular_price { value currency }
}
}
media_gallery {
url
label
}
}
}
}
`;
export default function ProductPage() {
const router = useRouter();
const { sku } = router.query;
const { loading, error, data } = useQuery(GET_PRODUCT, {
variables: { sku },
skip: !sku
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error</div>;
const product = data.products.items[0];
return (
<div>
<h1>{product.name}</h1>
<div dangerouslySetInnerHTML={{ __html: product.description.html }} />
<button>Add to Cart</button>
</div>
);
}
Vue Storefront
Vue Storefront Setup
Installation
# Using Vue Storefront
npx @vue-storefront/cli init magento-storefront
cd magento-storefront
# Configure Magento connection
# config/magento.js
module.exports = {
magento: {
url: 'https://magento-store.com',
graphql: '/graphql',
store: 'default'
}
};
Composable Pattern
// composables/useProduct.ts
import { useQuery } from '@vue/apollo-composable';
import { gql } from 'graphql-tag';
const GET_PRODUCTS = gql`
query GetProducts($search: String, $pageSize: Int) {
products(search: $search, pageSize: $pageSize) {
items {
id
name
sku
price_range {
minimum_price {
regular_price { value }
}
}
}
}
}
`;
export function useProduct(search?: string) {
const { result, loading, error } = useQuery(GET_PRODUCTS, {
search,
pageSize: 20
});
return {
products: computed(() => result.value?.products?.items ?? []),
loading,
error
};
}
Product Card Component
<!-- components/ProductCard.vue -->
<template>
<div class="product-card">
<img :src="product.image.url" :alt="product.name" />
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price_range.minimum_price.regular_price.value }}</p>
<button @click="addToCart">Add to Cart</button>
</div>
</template>
<script setup lang="ts">
import { useCart } from '@/composables/useCart';
const props = defineProps({
product: { type: Object, required: true }
});
const { addItem } = useCart();
const addToCart = () => {
addItem({
sku: props.product.sku,
quantity: 1
});
};
</script>
SSR and SSG
Server-Side Rendering
Next.js SSR
// pages/products.tsx
import { GetServerSideProps } from 'next';
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const client = new ApolloClient({
uri: process.env.MAGENTO_GRAPHQL,
cache: new InMemoryCache()
});
const { data } = await client.query({
query: gql`
query {
products(pageSize: 20) {
items { id name sku }
}
}
`
});
return {
props: {
products: data.products.items
}
};
};
export default function Products({ products }) {
return (
<div>
{products.map(p => <div key={p.id}>{p.name}</div>)}
</div>
);
}
Static Site Generation
// pages/product/[sku].tsx
import { GetStaticPaths, GetStaticProps } from 'next';
import { ApolloClient, InMemoryCache, gql } from '@apollo/client';
export const getStaticPaths: GetStaticPaths = async () => {
const client = new ApolloClient({
uri: process.env.MAGENTO_GRAPHQL,
cache: new InMemoryCache()
});
const { data } = await client.query({
query: gql`
query {
products(pageSize: 1000) {
items { sku }
}
}
`
});
const paths = data.products.items.map((product: any) => ({
params: { sku: product.sku }
}));
return { paths, fallback: 'blocking' };
};
export const getStaticProps: GetStaticProps = async ({ params }) => {
const { sku } = params;
const client = new ApolloClient({
uri: process.env.MAGENTO_GRAPHQL,
cache: new InMemoryCache()
});
const { data } = await client.query({
query: gql`
query GetProduct($sku: String!) {
products(filter: { sku: { eq: $sku } }) {
items { name sku description { html } }
}
}
`,
variables: { sku }
});
return {
props: { product: data.products.items[0] },
revalidate: 3600 // ISR: revalidate every hour
};
};
ISR (Incremental Static Regeneration)
// Revalidate static pages periodically
export const getStaticProps: GetStaticProps = async () => {
// Fetch data
return {
props: { data },
revalidate: 60 // Revalidate every 60 seconds
};
};
// Benefits:
- Static performance
- Dynamic updates
- No rebuild required
Authentication & State
Authentication
JWT Token Flow
// lib/auth.ts
export async function login(email: string, password: string) {
const response = await fetch(process.env.MAGENTO_GRAPHQL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: `mutation {
generateCustomerToken(email: "${email}", password: "${password}") {
token
}
}`
})
});
const { data } = await response.json();
return data.generateCustomerToken.token;
}
// Store token in httpOnly cookie
export async function setAuthCookie(token: string) {
document.cookie = `auth_token=${token}; path=/; secure; httponly`;
}
Cart State Management
// stores/cart.ts (using Zustand)
import create from 'zustand';
import { gql } from '@apollo/client';
const CREATE_CART = gql`
mutation CreateCart {
createEmptyCart
}
`;
const ADD_TO_CART = gql`
mutation AddToCart($cartId: String!, $sku: String!, $qty: Int!) {
addSimpleProductsToCart(
input: {
cart_id: $cartId
cart_items: [{ data: { quantity: $qty, sku: $sku } }]
}
) {
cart {
id
items { sku quantity }
prices { grand_total { value } }
}
}
}
`;
interface CartState {
cartId: string | null;
items: Array<{ sku: string; quantity: number }>;
createCart: () => Promise<void>;
addItem: (sku: string, qty: number) => Promise<void>;
}
export const useCartStore = create<CartState>((set, get) => ({
cartId: null,
items: [],
createCart: async () => {
const { data } = await client.mutate({ mutation: CREATE_CART });
set({ cartId: data.createEmptyCart });
},
addItem: async (sku, qty) => {
let { cartId } = get();
if (!cartId) {
await get().createCart();
cartId = get().cartId;
}
const { data } = await client.mutate({
mutation: ADD_TO_CART,
variables: { cartId, sku, qty }
});
set({ items: data.addSimpleProductsToCart.cart.items });
}
}));
SSR Authentication
// Protected page with SSR
export const getServerSideProps: GetServerSideProps = async ({ req }) => {
const token = req.cookies.auth_token;
if (!token) {
return { redirect: { destination: '/login', permanent: false } };
}
// Verify token and get user data
const user = await verifyToken(token);
return {
props: { user }
};
};
Practice Problems
Build a product listing page with React, GraphQL, and infinite scroll.
Implement server-side rendering for product pages with ISR.
Quiz
1. What is the difference between SSR and SSG?
2. What is ISR?
3. What state management works well with headless?
4. How to authenticate in headless Magento?
Flashcards
Question
What is SSR?
Click to reveal answer
Answer
Server-Side Rendering: renders page on each request
Question
What is SSG?
Click to reveal answer
Answer
Static Site Generation: generates pages at build time
Question
What is ISR?
Click to reveal answer
Answer
Incremental Static Regeneration: static pages update periodically
Question
How to authenticate headless?
Click to reveal answer
Answer
JWT tokens via GraphQL mutation
Question
What is Apollo Client?
Click to reveal answer
Answer
GraphQL client for React with caching and state management
Revision Notes
Key Takeaways
- 1. React/Vue with Apollo Client for GraphQL integration
- 2. SSR: renders on request (good for dynamic content)
- 3. SSG: generates at build time (best performance)
- 4. ISR: static with periodic revalidation (best of both)
- 5. JWT tokens for authentication in SPAs
- 6. Zustand/Redux/Context for state management
Interview Tips
- • How do you set up a React storefront with Magento?
- • Explain SSR vs SSG vs ISR
- • How do you handle authentication in a headless app?
- • Describe your state management approach
- • How do you optimize performance in a headless frontend?
Cheat Sheet
Headless Frontend Cheat Sheet
Stack:
- React/Vue + Next.js/Nuxt.js
- Apollo Client for GraphQL
- Zustand/Redux for state
Rendering:
- SSR: dynamic, per-request
- SSG: static, build-time
- ISR: static + revalidation
Auth:
- JWT via GraphQL mutation
- httpOnly cookies
- Token refresh
State:
- Cart: server-side (Magento)
- UI: client-side (Zustand)
- User: JWT + context
Performance:
- Code splitting
- Lazy loading
- CDN caching
- Image optimization