Bevezetés: Miért éri meg újragondolni az adatlekérdezést?
Ha az elmúlt pár évben bármit fejlesztettél Next.js-ben, akkor biztosan te is érezted: a React Server Components (RSC) teljesen felforgatták, ahogyan adatokat kérünk le és jelenítünk meg. A régi jó useEffect + SWR kombó? Már nem az egyetlen — sőt, egyre kevésbé az elsődleges — megközelítés.
Helyette itt van egy modell, ahol a komponensek a szerveren futnak, közvetlenül hozzáférnek az adatforrásokhoz, és csak a kész HTML-t küldik el a böngészőnek. Egyszerűen hangzik, igaz? Na, a részletek azért érdekesek.
A Next.js 15 és az App Router architektúra tovább finomította ezeket a lehetőségeket. A streaming, a React Suspense, a párhuzamos adatlekérdezés és a fejlett gyorsítótárazási stratégiák együtt olyan alkalmazásokat tesznek lehetővé, amelyek gyorsabbak és reszponzívabbak, mint valaha. De hogyan használjuk mindezt a gyakorlatban? Milyen csapdákat kerüljünk el? És mikor melyik megközelítés a nyerő?
Ebben az útmutatóban végigmegyünk a Next.js 15 összes lényeges adatlekérdezési mintáján — konkrét kódpéldákkal, teljesítmény-összehasonlításokkal és architekturális ajánlásokkal. Akár most ismerkedsz a Server Components-szel, akár tapasztalt fejlesztőként keresed a legújabb optimalizálási trükköket, találsz itt hasznosat.
A React Server Components alapjai: Miért változik meg minden?
Server Components vs. Client Components
A Next.js App Routerben minden komponens alapértelmezetten Server Component. Ez egyszerűen annyit jelent, hogy a komponens kódja kizárólag a szerveren fut, és soha nem kerül el a böngészőbe.
Ennek számos előnye van:
- Közvetlen adatforrás-hozzáférés — Az adatbázist, fájlrendszert vagy belső API-kat közvetlenül, extra HTTP kérések nélkül érheted el.
- Kisebb kliens oldali bundle — A szerver komponens kódja nem kerül a böngészőbe, így a JavaScript bundle mérete drasztikusan csökken.
- Nincs szükség külön API rétegre — Nem kell API végpontokat építgetni csak az adatlekérdezéshez.
- Biztonság — Az érzékeny adatok (API kulcsok, adatbázis-kapcsolati sztringek) soha nem hagyják el a szervert.
// app/dashboard/page.tsx — Ez egy Server Component (alapértelmezett)
// Közvetlen adatbázis-hozzáférés, nincs szükség API-ra
import { db } from '@/lib/database';
export default async function DashboardPage() {
// Ez közvetlenül a szerveren fut le
const stats = await db.query('SELECT * FROM dashboard_stats');
const recentOrders = await db.query(
'SELECT * FROM orders ORDER BY created_at DESC LIMIT 10'
);
return (
<div>
<h1>Vezérlőpult</h1>
<StatsGrid stats={stats} />
<RecentOrdersList orders={recentOrders} />
</div>
);
}
Ezzel szemben a Client Components (amelyeket a 'use client' direktívával jelölünk) a böngészőben futnak. Ezekre akkor van szükséged, ha interaktív elemeket használsz — állapotkezelés, eseménykezelők, böngésző API-k, ilyesmi.
Az adatlekérdezés új aranyszabálya: „Kérd le ott, ahol megjelenítesz"
Őszintén szólva, ez volt számomra az egyik legnagyobb szemléletváltás. A lényeg, hogy az adatlekérdezést a megjelenítő komponenshez társítjuk, nem a szülő komponensben kérjük le az adatokat, hogy aztán prop-ként továbbadjuk. Miért jó ez?
- Nincs többé felesleges prop drilling
- Minden komponens önállóan felelős a saját adataiért
- A React automatikusan deduplikálja az azonos kéréseket
- Könnyebb a kód karbantartása és tesztelése
// Régi megközelítés — KERÜLENDŐ: Prop drilling
// app/page.tsx
async function Page() {
const user = await getUser();
const posts = await getPosts();
const comments = await getComments();
return (
<Layout user={user}>
<PostList posts={posts} comments={comments} />
</Layout>
);
}
// Új megközelítés — AJÁNLOTT: Kolokált adatlekérdezés
// app/page.tsx
async function Page() {
return (
<Layout>
<PostList />
</Layout>
);
}
// components/Layout.tsx — Saját adatait maga kéri le
async function Layout({ children }: { children: React.ReactNode }) {
const user = await getUser();
return <nav>{user.name}</nav>;
}
// components/PostList.tsx — Saját adatait maga kéri le
async function PostList() {
const posts = await getPosts();
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>;
}
Streaming és React Suspense: Amikor az oldal darabokban érkezik
Mi az a streaming és miért fontos?
A streaming lényege, hogy az oldal HTML-jét kisebb darabokra (chunk-okra) bontjuk, és fokozatosan küldjük el a böngészőnek, ahogy az egyes részek elkészülnek. Gondolj erre úgy, mint amikor egy étteremben az ételeket nem egyszerre hozzák ki, hanem ahogy elkészülnek.
- Streaming nélkül: A felhasználó üres képernyőt lát, amíg az összes adat betöltődik. Aztán — bumm — egyszerre jelenik meg minden.
- Streaminggel: Az oldal gyorsan elküldi az alapvető struktúrát, és a lassabb részek fokozatosan jelennek meg.
A Next.js App Routerben a streaming kétféleképpen valósítható meg: a loading.tsx fájl konvencióval és a React Suspense boundary-kkel.
A loading.tsx automatikus streaming
A legegyszerűbb mód a streaming bevezetéséhez: hozz létre egy loading.tsx fájlt az adott route mappájában. Ez automatikusan Suspense boundary-ként működik, és amíg az oldal adatai töltődnek, a loading.tsx tartalmát jeleníti meg.
// app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/3 mb-6" />
<div className="grid grid-cols-3 gap-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-32 bg-gray-200 rounded" />
))}
</div>
<div className="mt-6 space-y-3">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-12 bg-gray-200 rounded" />
))}
</div>
</div>
);
}
// app/dashboard/page.tsx
// Ez az oldal automatikusan a loading.tsx-et mutatja betöltés közben
export default async function DashboardPage() {
const data = await fetchDashboardData(); // Ez akár 3 másodpercig is tarthat
return <Dashboard data={data} />;
}
Granulált streaming Suspense boundary-kkel
A loading.tsx az egész oldal szintjén működik. De mi van, ha finomabb kontrollra van szükséged? Ilyenkor a React Suspense komponenst használod közvetlenül, hogy az oldal egyes részeit külön-külön streameld:
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { StatsCards } from '@/components/StatsCards';
import { RevenueChart } from '@/components/RevenueChart';
import { RecentOrders } from '@/components/RecentOrders';
import { ActivityFeed } from '@/components/ActivityFeed';
export default function DashboardPage() {
return (
<div className="grid grid-cols-12 gap-6">
{/* A statisztikák gyorsan betöltődnek */}
<div className="col-span-12">
<Suspense fallback={<StatsCardsSkeleton />}>
<StatsCards />
</Suspense>
</div>
{/* A bevételi grafikon lassabban töltődik */}
<div className="col-span-8">
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</div>
{/* Az aktivitási feed külön streamelődik */}
<div className="col-span-4">
<Suspense fallback={<FeedSkeleton />}>
<ActivityFeed />
</Suspense>
</div>
{/* A legutóbbi rendelések szintén külön */}
<div className="col-span-12">
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
</div>
);
}
Ezzel a mintával minden szekció függetlenül töltődik be. Ha a statisztikák 200ms alatt visszajönnek, de a bevételi grafikon 2 másodpercet vesz igénybe, a felhasználó azonnal látja a statisztikákat, miközben a grafikon helyén egy skeleton loader pörög. Ez drámaian javítja az LCP és TTI metrikákat — ami a Google szemében sem mindegy.
Párhuzamos adatlekérdezés: Szabadulj meg a vízesés-hatástól
A szekvenciális lekérdezés csapdája
Az egyik leggyakoribb teljesítményprobléma (és bevallom, én is belefutottam párszor) az úgynevezett „waterfall" hatás. Ez akkor fordul elő, amikor az adatlekérdezések egymás után futnak le, és mindegyik megvárja az előzőt:
// KERÜLENDŐ: Szekvenciális vízesés minta
export default async function ProductPage({ params }: { params: { id: string } }) {
// 1. lekérdezés: 300ms
const product = await getProduct(params.id);
// 2. lekérdezés: 200ms (megvárja az 1.-et)
const reviews = await getReviews(params.id);
// 3. lekérdezés: 250ms (megvárja az 1. és 2.-t)
const relatedProducts = await getRelatedProducts(product.categoryId);
// Összesen: 300 + 200 + 250 = 750ms
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
<RelatedProducts products={relatedProducts} />
</div>
);
}
750 milliszekundum. Nem hangzik soknak, de a felhasználók érzik a különbséget.
Megoldás 1: Promise.all() párhuzamos lekérdezésekkel
Ha az adatlekérdezések egymástól függetlenek, a Promise.all() segítségével párhuzamosan futtathatjuk őket:
// AJÁNLOTT: Párhuzamos adatlekérdezés Promise.all()-lal
export default async function ProductPage({ params }: { params: { id: string } }) {
// Mindkét lekérdezés egyszerre indul
const [product, reviews] = await Promise.all([
getProduct(params.id),
getReviews(params.id),
]);
// Ez függ a product-tól, ezért utána fut
const relatedProducts = await getRelatedProducts(product.categoryId);
// Összesen: max(300, 200) + 250 = 550ms (27%-kal gyorsabb!)
return (
<div>
<ProductDetails product={product} />
<ReviewsList reviews={reviews} />
<RelatedProducts products={relatedProducts} />
</div>
);
}
Megoldás 2: Suspense + párhuzamos komponens renderelés
Na, és most jön az igazán elegáns megoldás. A Suspense és a Server Components kombinálásával minden komponens maga kéri le az adatait, a Next.js pedig automatikusan párhuzamosítja a renderelést:
// A LEGJOBB: Suspense + kolokált adatlekérdezés
import { Suspense } from 'react';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* Mindhárom komponens párhuzamosan kezdi az adatlekérdezést */}
<Suspense fallback={<ProductSkeleton />}>
<ProductDetails productId={params.id} />
</Suspense>
<Suspense fallback={<ReviewsSkeleton />}>
<ReviewsList productId={params.id} />
</Suspense>
<Suspense fallback={<RelatedSkeleton />}>
<RelatedProducts productId={params.id} />
</Suspense>
</div>
);
}
// components/ProductDetails.tsx
async function ProductDetails({ productId }: { productId: string }) {
const product = await getProduct(productId);
return (
<section>
<h1>{product.name}</h1>
<p>{product.description}</p>
<span>{product.price} Ft</span>
</section>
);
}
// components/ReviewsList.tsx
async function ReviewsList({ productId }: { productId: string }) {
const reviews = await getReviews(productId);
return (
<section>
<h2>Vélemények ({reviews.length})</h2>
{reviews.map(r => (
<div key={r.id}>
<p>{r.text}</p>
<span>{r.rating}/5</span>
</div>
))}
</section>
);
}
Ennek a megközelítésnek az a szépsége, hogy a felhasználó azonnal látja azt a szekciót, amelyik elsőként elkészül. Nem kell az egész oldalra várnia.
A React use() API: Adatok streamelése szervertől kliensig
A use() API működése
A React 19-ben megjelent use() API egy teljesen újfajta módszert kínál az adatok átadására Server Componentből Client Componentbe. A trükk az, hogy a szerveren létrehozunk egy Promise-t, de nem await-eljük, hanem közvetlenül továbbadjuk a kliens komponensnek. Az aztán a use() hook-kal bontja ki az értéket.
Kicsit szokatlan elsőre, de megéri megszokni.
// app/dashboard/page.tsx — Server Component
import { ClientDashboard } from './ClientDashboard';
export default function DashboardPage() {
// NEM await-eljük — átadjuk a Promise-t!
const analyticsPromise = fetchAnalytics();
const notificationsPromise = fetchNotifications();
return (
<div>
<h1>Vezérlőpult</h1>
{/* A Promise-okat prop-ként adjuk át */}
<ClientDashboard
analyticsPromise={analyticsPromise}
notificationsPromise={notificationsPromise}
/>
</div>
);
}
// app/dashboard/ClientDashboard.tsx — Client Component
'use client';
import { use, Suspense } from 'react';
interface Props {
analyticsPromise: Promise<AnalyticsData>;
notificationsPromise: Promise<Notification[]>;
}
export function ClientDashboard({ analyticsPromise, notificationsPromise }: Props) {
return (
<div>
<Suspense fallback={<p>Analitika betöltése...</p>}>
<AnalyticsPanel promise={analyticsPromise} />
</Suspense>
<Suspense fallback={<p>Értesítések betöltése...</p>}>
<NotificationsPanel promise={notificationsPromise} />
</Suspense>
</div>
);
}
function AnalyticsPanel({ promise }: { promise: Promise<AnalyticsData> }) {
// A use() hook kibontja a Promise értékét
const analytics = use(promise);
return (
<div>
<h2>Analitika</h2>
<p>Látogatók: {analytics.visitors}</p>
<p>Konverzió: {analytics.conversionRate}%</p>
</div>
);
}
Mikor érdemes a use() API-t használni?
A use() API-nak megvan a maga helye, különösen ezekben az esetekben:
- Interaktív dashboardok — Ahol a szerver indítja az adatlekérdezést, de a kliens kezelni szeretné az eredményt (pl. grafikonok rajzolásához).
- Progresszív feltöltés — Az oldal shell-je azonnal megjelenik, míg a lassabb adatok fokozatosan érkeznek.
- Hibrid komponensek — Amikor a szerver oldali adatlekérdezés előnyeit akarod ötvözni a kliens oldali interaktivitással.
Renderelési stratégiák: SSR, SSG, ISR és PPR
Static Site Generation (SSG) — Statikus generálás
A statikus generálás során az oldal build időben renderelődik, és CDN-ről lesz kiszolgálva. Ez a leggyorsabb opció — de nyilván csak olyan oldalaknál működik, amelyek tartalma ritkán változik:
// app/blog/[slug]/page.tsx — Statikusan generált blog oldal
import { db } from '@/lib/database';
// Build időben generáljuk az összes lehetséges útvonalat
export async function generateStaticParams() {
const posts = await db.query('SELECT slug FROM posts WHERE published = true');
return posts.map((post) => ({ slug: post.slug }));
}
// Ez a komponens build időben fut le — egyszer
export default async function BlogPost({ params }: { params: { slug: string } }) {
const post = await db.query(
'SELECT * FROM posts WHERE slug = ?',
[params.slug]
);
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
);
}
// Opcionális: Force static megjelölés
export const dynamic = 'force-static';
Incremental Static Regeneration (ISR) — Inkrementális statikus újragenerálás
Az ISR a statikus generálás és a szerver oldali renderelés legjobb tulajdonságait ötvözi. Az oldal statikusan kiszolgálható, de megadott időközönként automatikusan frissül a háttérben. Gyakorlatilag a legjobb mindkét világból:
// app/products/page.tsx — ISR revalidation-nel
export const revalidate = 3600; // 1 órás gyorsítótár
export default async function ProductsPage() {
const products = await fetch('https://api.example.com/products', {
next: { revalidate: 3600 }, // Fetch szintű revalidation
}).then(res => res.json());
return (
<div className="grid grid-cols-3 gap-4">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
);
}
// Igény szerinti (on-demand) revalidation Server Action-nel
// app/actions/revalidate.ts
'use server';
import { revalidatePath, revalidateTag } from 'next/cache';
export async function revalidateProducts() {
// Útvonal alapú újravalidálás
revalidatePath('/products');
// Vagy tag alapú — finomabb kontroll
revalidateTag('products');
}
Server-Side Rendering (SSR) — Szerver oldali renderelés
Az SSR minden kérésnél frissen rendereli az oldalt. Akkor használd, ha az oldal tartalma minden kérésnél más — személyre szabott tartalom, valós idejű adatok, ilyesmi:
// app/feed/page.tsx — Dinamikus SSR
export const dynamic = 'force-dynamic'; // Minden kérésnél frissen renderel
export default async function FeedPage() {
const feed = await getPersonalizedFeed();
return (
<div>
<h1>Az Ön hírfolyama</h1>
{feed.map((item) => (
<FeedItem key={item.id} item={item} />
))}
</div>
);
}
Partial Prerendering (PPR) — A Next.js 15 egyik legizgalmasabb újítása
Ezt a feature-t imádom. A PPR lehetővé teszi, hogy egyazon oldalon belül egyes részek statikusan legyenek előrenderelve, míg más részek dinamikusan töltődjenek be. Tehát lényegében SSG és SSR egyetlen oldalon belül:
// next.config.ts — PPR engedélyezése
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
experimental: {
ppr: true, // Partial Prerendering bekapcsolása
},
};
export default nextConfig;
// app/product/[id]/page.tsx — Hibrid oldal
import { Suspense } from 'react';
export default function ProductPage({ params }: { params: { id: string } }) {
return (
<div>
{/* Ez STATIKUSAN előrenderelt — build időben */}
<header>
<nav>Webáruház Menü</nav>
</header>
{/* Ez is statikus — a termék adatai ritkán változnak */}
<Suspense fallback={<ProductSkeleton />}>
<ProductInfo productId={params.id} />
</Suspense>
{/* Ez DINAMIKUS — felhasználó-specifikus ajánlások */}
<Suspense fallback={<RecommendationsSkeleton />}>
<PersonalizedRecommendations productId={params.id} />
</Suspense>
{/* Ez is DINAMIKUS — valós idejű készletadatok */}
<Suspense fallback={<StockSkeleton />}>
<LiveStockStatus productId={params.id} />
</Suspense>
{/* Ez STATIKUS — lábrész */}
<footer>© 2026 Webáruház</footer>
</div>
);
}
A PPR segítségével az oldal statikus részeit a CDN-ről azonnali válaszidővel szolgálod ki, míg a dinamikus részek a háttérben streamelődnek. Az oldal „váza" azonnal megjelenik — és ez hatalmas különbség a felhasználói élményben.
Data Access Layer: Tartsd rendben az adathozzáférést
Miért van szükség Data Access Layer-re?
Ahogy a korábbi middleware biztonsági cikkünkben is bemutattuk, a mélységi védelem egyik kulcseleme az adathozzáférés szintjén végzett jogosultságellenőrzés. A Data Access Layer (DAL) egy köztes réteg, amely centralizálja az adatbázis-lekérdezéseket, és biztosítja, hogy minden adathozzáférés megfelelően validált legyen.
Gondolj rá úgy, mint egy biztonsági őrre az adatbázisod előtt.
// lib/dal/products.ts — Data Access Layer
import { db } from '@/lib/database';
import { auth } from '@/lib/auth';
import { cache } from 'react';
// A React cache() deduplikálja az azonos render cikluson belüli hívásokat
export const getProduct = cache(async (productId: string) => {
const product = await db.query(
'SELECT * FROM products WHERE id = ? AND active = true',
[productId]
);
if (!product) {
throw new Error('A termék nem található');
}
return product;
});
// Jogosultságellenőrzéssel védett lekérdezés
export const getAdminProducts = cache(async () => {
const session = await auth();
if (!session || session.user.role !== 'admin') {
throw new Error('Nincs jogosultsága ehhez a művelethez');
}
return db.query('SELECT * FROM products ORDER BY created_at DESC');
});
// Szűrt lekérdezés keresési paraméterekkel
export const searchProducts = cache(async (query: string, page: number = 1) => {
const limit = 20;
const offset = (page - 1) * limit;
const [products, total] = await Promise.all([
db.query(
'SELECT * FROM products WHERE name LIKE ? LIMIT ? OFFSET ?',
[`%${query}%`, limit, offset]
),
db.query(
'SELECT COUNT(*) as count FROM products WHERE name LIKE ?',
[`%${query}%`]
),
]);
return {
products,
totalPages: Math.ceil(total.count / limit),
currentPage: page,
};
});
A React cache() szerepe
A cache() függvény a React beépített memoizálási megoldása. Garantálja, hogy ugyanazon render cikluson belül az azonos argumentumokkal hívott függvény csak egyszer hajtódik végre ténylegesen. Ez különösen hasznos, ha több komponens is ugyanazt az adatot kéri le:
// Ha mindkét komponens meghívja a getProduct('123')-at,
// a tényleges adatbázis-lekérdezés csak egyszer történik meg
// components/ProductHeader.tsx
async function ProductHeader({ productId }: { productId: string }) {
const product = await getProduct(productId); // 1. hívás
return <h1>{product.name}</h1>;
}
// components/ProductPrice.tsx
async function ProductPrice({ productId }: { productId: string }) {
const product = await getProduct(productId); // Deduplikált — nincs új DB query
return <span>{product.price} Ft</span>;
}
Hibakezelés az adatlekérdezésben
Error Boundary-k használata
A Next.js App Router lehetővé teszi, hogy az error.tsx fájl konvenciót használd az adatlekérdezési hibák kezelésére. Ez automatikusan React Error Boundary-ként funkcionál:
// app/dashboard/error.tsx
'use client'; // Az error.tsx-nek Client Component-nek kell lennie
export default function DashboardError({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
return (
<div className="p-6 bg-red-50 rounded-lg">
<h2 className="text-red-800 text-lg font-bold">
Hiba történt a vezérlőpult betöltése közben
</h2>
<p className="text-red-600 mt-2">
{error.message || 'Ismeretlen hiba. Kérjük, próbálja újra.'}
</p>
<button
onClick={reset}
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Újrapróbálás
</button>
</div>
);
}
Granulált hibakezelés Suspense-szel
Ha finomabb hibakezelésre van szükséged (és gyakran van), az egyes szekciókhoz saját Error Boundary-t rendelhetsz:
// components/ErrorBoundaryWrapper.tsx
'use client';
import { Component, ReactNode } from 'react';
interface Props {
children: ReactNode;
fallback: ReactNode;
}
interface State {
hasError: boolean;
}
export class ErrorBoundaryWrapper extends Component<Props, State> {
state = { hasError: false };
static getDerivedStateFromError(): State {
return { hasError: true };
}
render() {
if (this.state.hasError) {
return this.props.fallback;
}
return this.props.children;
}
}
// Használat az oldalban
import { Suspense } from 'react';
import { ErrorBoundaryWrapper } from '@/components/ErrorBoundaryWrapper';
export default function DashboardPage() {
return (
<div>
<ErrorBoundaryWrapper fallback={<p>Nem sikerült betölteni a statisztikákat.</p>}>
<Suspense fallback={<StatsSkeleton />}>
<StatsCards />
</Suspense>
</ErrorBoundaryWrapper>
<ErrorBoundaryWrapper fallback={<p>A grafikon jelenleg nem elérhető.</p>}>
<Suspense fallback={<ChartSkeleton />}>
<RevenueChart />
</Suspense>
</ErrorBoundaryWrapper>
</div>
);
}
Gyakorlati példa: Teljes dashboard implementáció
Szóval, most rakjuk össze az eddig tanultakat egy komplett, éles alkalmazásra kész dashboard implementációban. Ez az a rész, ahol az elmélet gyakorlattá válik:
// lib/dal/dashboard.ts — Data Access Layer
import { db } from '@/lib/database';
import { auth } from '@/lib/auth';
import { cache } from 'react';
export const getDashboardStats = cache(async () => {
const session = await auth();
if (!session) throw new Error('Nem hitelesített');
return db.query(`
SELECT
COUNT(*) as totalOrders,
SUM(amount) as totalRevenue,
AVG(amount) as avgOrderValue,
COUNT(DISTINCT customer_id) as uniqueCustomers
FROM orders
WHERE merchant_id = ?
AND created_at >= datetime('now', '-30 days')
`, [session.user.merchantId]);
});
export const getRevenueHistory = cache(async () => {
const session = await auth();
if (!session) throw new Error('Nem hitelesített');
return db.query(`
SELECT
DATE(created_at) as date,
SUM(amount) as revenue
FROM orders
WHERE merchant_id = ?
AND created_at >= datetime('now', '-90 days')
GROUP BY DATE(created_at)
ORDER BY date ASC
`, [session.user.merchantId]);
});
export const getRecentOrders = cache(async (limit: number = 10) => {
const session = await auth();
if (!session) throw new Error('Nem hitelesített');
return db.query(`
SELECT o.*, c.name as customerName
FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.merchant_id = ?
ORDER BY o.created_at DESC
LIMIT ?
`, [session.user.merchantId, limit]);
});
// app/dashboard/page.tsx — Főoldal streaming-gel
import { Suspense } from 'react';
import { StatsOverview } from './components/StatsOverview';
import { RevenueChart } from './components/RevenueChart';
import { OrdersTable } from './components/OrdersTable';
export default function DashboardPage() {
return (
<main className="max-w-7xl mx-auto px-4 py-8">
<h1 className="text-3xl font-bold mb-8">Vezérlőpult</h1>
<section className="mb-8">
<Suspense fallback={<StatsOverviewSkeleton />}>
<StatsOverview />
</Suspense>
</section>
<section className="mb-8">
<Suspense fallback={<RevenueChartSkeleton />}>
<RevenueChart />
</Suspense>
</section>
<section>
<Suspense fallback={<OrdersTableSkeleton />}>
<OrdersTable />
</Suspense>
</section>
</main>
);
}
// app/dashboard/components/StatsOverview.tsx
import { getDashboardStats } from '@/lib/dal/dashboard';
export async function StatsOverview() {
const stats = await getDashboardStats();
const cards = [
{ label: 'Összes rendelés', value: stats.totalOrders },
{ label: 'Bevétel', value: `${stats.totalRevenue.toLocaleString()} Ft` },
{ label: 'Átlagos rendelésérték', value: `${Math.round(stats.avgOrderValue).toLocaleString()} Ft` },
{ label: 'Egyedi vásárlók', value: stats.uniqueCustomers },
];
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{cards.map((card) => (
<div key={card.label} className="bg-white rounded-xl shadow p-6">
<p className="text-gray-500 text-sm">{card.label}</p>
<p className="text-2xl font-bold mt-1">{card.value}</p>
</div>
))}
</div>
);
}
Teljesítmény-optimalizálási tippek
1. Kerüld a szükségtelen dinamikus renderelést
A Next.js 15 alapértelmezetten megpróbálja statikusan renderelni az oldalakat. Ha a cookies(), headers() vagy searchParams API-kat használod, az oldal automatikusan dinamikussá válik. Csak akkor nyúlj ezekhez, ha tényleg szükséges:
// KERÜLENDŐ: Szükségtelenül dinamikus
export default async function Page() {
const headersList = await headers(); // Ez dinamikussá teszi az egész oldalt!
const products = await getProducts();
return <ProductList products={products} />;
}
// AJÁNLOTT: Maradjon statikus, ha lehetséges
export default async function Page() {
const products = await getProducts();
return <ProductList products={products} />;
}
2. Válaszd ki a megfelelő revalidation stratégiát
Nem minden adatnak kell valós időben frissülnie. Érdemes végiggondolni, melyik tartalom milyen gyakran változik:
- Statikus (SSG) — Marketing oldalak, dokumentáció, GYIK
- ISR (60-3600 mp) — Termékkatalógus, blog bejegyzések, kategória oldalak
- Dinamikus (SSR) — Személyre szabott tartalom, kosár, felhasználói profil
- Kliens oldali — Valós idejű chat, live értesítések, interaktív vizualizáció
3. Használj párhuzamos route szegmenseket
A Next.js párhuzamos route-ok (@slot) lehetővé teszik, hogy egy layout-on belül több szekciót párhuzamosan renderelj, mindegyiket saját loading és error állapotokkal. Ez a feature különösen dashboard jellegű oldalaknál hasznos:
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
analytics,
notifications,
}: {
children: React.ReactNode;
analytics: React.ReactNode;
notifications: React.ReactNode;
}) {
return (
<div className="grid grid-cols-12 gap-6">
<main className="col-span-8">{children}</main>
<aside className="col-span-4 space-y-6">
{analytics}
{notifications}
</aside>
</div>
);
}
// app/dashboard/@analytics/page.tsx
export default async function AnalyticsSlot() {
const data = await getAnalytics();
return <AnalyticsWidget data={data} />;
}
// app/dashboard/@analytics/loading.tsx
export default function AnalyticsLoading() {
return <AnalyticsWidgetSkeleton />;
}
// app/dashboard/@notifications/page.tsx
export default async function NotificationsSlot() {
const notifications = await getNotifications();
return <NotificationsList items={notifications} />;
}
Összefoglalás: Melyik mintát válaszd?
A Next.js 15 App Router adatlekérdezési lehetőségei rendkívül gazdagok. A megfelelő minta kiválasztása alapvető fontosságú az alkalmazás teljesítménye és karbantarthatósága szempontjából. Íme egy gyors döntési segédlet:
- Az adat ritkán változik? → Használj SSG-t vagy ISR-t a
generateStaticParamsésrevalidatesegítségével. - Az adat felhasználó-specifikus? → Használj dinamikus SSR-t a
cookies()vagyheaders()API-kkal. - Egy oldalon belül kevert a frissítési igény? → PPR a megoldás a statikus és dinamikus részek elkülönítésére.
- Több lassú adatforrásod van? → Suspense boundary-k a párhuzamos streameléshöz.
- Kliens oldalon kell dolgozni az adatokkal? → A
use()API a barátod. - Több komponens kéri le ugyanazt az adatot? →
cache()a deduplikáláshoz.
A legfontosabb elvek, amiket érdemes fejben tartani:
- Alapértelmezetten Server Component — Csak akkor használj Client Component-et, ha tényleg muszáj.
- Kolokáld az adatlekérdezést — Minden komponens maga kérje le a saját adatait.
- Kerüld a vízesés-hatást — Párhuzamos lekérdezések és Suspense boundary-k a barátaid.
- Használj Data Access Layer-t — Centralizáld az adathozzáférést és a jogosultságellenőrzést.
- Gondolj a gyorsítótárazásra — Válaszd ki a megfelelő revalidation stratégiát minden adatforráshoz.
A React Server Components és a Next.js App Router együtt egy olyan adatlekérdezési modellt adnak a kezedbe, amely egyszerűbb, biztonságosabb és hatékonyabb, mint bármi, amit korábban láttunk a React ökoszisztémában. Ezeknek a mintáknak az elsajátításával olyan alkalmazásokat építhetsz, amelyek egyszerre nyújtanak kiváló felhasználói élményt és fejlesztői kényelmet.