Next.js is the most popular React framework for building full-stack web applications with server-side rendering. For a PHP developer the concepts of SSR, static generation, and server-side data fetching are familiar – they are what PHP has always done. What is new is the React component model and the hybrid approach where some code runs on the server and some in the browser. I show Next.js from a PHP perspective with Magento 2 GraphQL as the backend.
Mental model for a PHP developer
| PHP/Magento concept | Next.js equivalent |
|---|---|
| PHP template (PHTML) | React Server Component |
| JavaScript/Alpine.js block | Client Component (with ‘use client’) |
| Controller + view | page.tsx + layout.tsx |
| API endpoint | route.ts (App Router API) |
| Session/cookie | Server-side cookies via next/headers |
| getParam() from URL | params and searchParams props |
| require() | import (ES modules) |
Server Components – PHP-like rendering
// app/products/[slug]/page.tsx
// This is a Server Component - runs on the server, like PHP
// No bundle size impact, can access databases/APIs directly
import { getMagentoProduct } from '@/lib/magento';
// Props receive URL params automatically - like $_GET in PHP
interface PageProps {
params: { slug: string };
searchParams: { store?: string };
}
export default async function ProductPage({ params, searchParams }: PageProps) {
// Await data directly - no useEffect, no loading states
// Runs on server, like a PHP controller
const product = await getMagentoProduct(params.slug, searchParams.store ?? 'default');
if (!product) {
return <div>Product not found</div>;
}
return (
<div className="max-w-5xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-2xl text-primary mt-2">{product.price.regularPrice.amount.value} PLN</p>
<div dangerouslySetInnerHTML={{ __html: product.description.html }} />
{/* Client Component for interactive add-to-cart */}
<AddToCartButton productId={product.id} sku={product.sku} />
</div>
);
}
// SEO metadata - generated on server, like PHP head block
export async function generateMetadata({ params }: PageProps) {
const product = await getMagentoProduct(params.slug);
return {
title: product?.name ?? 'Product',
description: product?.meta_description ?? '',
};
}
Client Component – interactive parts only
// components/AddToCartButton.tsx
// 'use client' marks this as a Client Component
// This is like an Alpine.js x-data block - only interactive parts need to be client-side
'use client';
import { useState } from 'react';
interface Props {
productId: number;
sku: string;
}
export function AddToCartButton({ productId, sku }: Props) {
const [qty, setQty] = useState(1);
const [adding, setAdding] = useState(false);
const [added, setAdded] = useState(false);
const addToCart = async () => {
setAdding(true);
try {
const res = await fetch('/api/cart/add', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sku, qty }),
});
if (res.ok) {
setAdded(true);
setTimeout(() => setAdded(false), 2000);
}
} finally {
setAdding(false);
}
};
return (
<div className="flex gap-4 mt-6">
<div className="flex items-center border rounded">
<button onClick={() => setQty(q => Math.max(1, q - 1))} className="px-3 py-2">-</button>
<span className="px-4">{qty}</span>
<button onClick={() => setQty(q => q + 1)} className="px-3 py-2">+</button>
</div>
<button
onClick={addToCart}
disabled={adding}
className={`px-8 py-2 rounded text-white ${added ? 'bg-green-500' : 'bg-blue-600'}`}
>
{adding ? 'Adding...' : added ? 'Added ✓' : 'Add to cart'}
</button>
</div>
);
}
Fetching from Magento GraphQL
// lib/magento.ts
const MAGENTO_GRAPHQL_URL = process.env.NEXT_PUBLIC_MAGENTO_URL + '/graphql';
export async function getMagentoProduct(slug: string, storeCode = 'default') {
const res = await fetch(MAGENTO_GRAPHQL_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Store': storeCode,
},
body: JSON.stringify({
query: `
query GetProduct($urlKey: String!) {
products(filter: { url_key: { eq: $urlKey } }) {
items {
id
sku
name
meta_description
description { html }
price_range {
minimum_price {
regular_price { value currency }
final_price { value currency }
}
}
media_gallery {
url
label
}
}
}
}
`,
variables: { urlKey: slug },
}),
// Next.js caching - cache product for 60 seconds, then revalidate
next: { revalidate: 60 },
});
const data = await res.json();
return data?.data?.products?.items?.[0] ?? null;
}
Summary
Next.js App Router with Server Components is conceptually closer to PHP than client-side React. Data fetching happens on the server, HTML is generated server-side, and only interactive parts (add to cart, quantity selector) ship as client-side JavaScript. This hybrid approach gives the SEO benefits of SSR with the interactivity of a SPA where needed. For Magento 2 headless projects, Next.js + GraphQL is a well-established combination with good community support.
