# React 19: Server Components у продакшені - повний посібник
> Опанування Server Components у React 19 для продакшену. Архітектура, патерни, стрімінг, кешування та оптимізації для високопродуктивних застосунків.
- Published: 2026-01-08
- Updated: 2026-04-06
- Author: SharpSkill
- Tags: react 19, server components, rsc, performance, next.js
- Reading time: 14 min
---
Server Components являють собою найзначнішу еволюцію в React з часів появи Hooks. У React 19 ця архітектура досягла зрілості та готова до використання у продакшені, дозволяючи компонентам виконуватися безпосередньо на сервері зі збереженням інтерактивності на стороні клієнта.
> **Передумови**
>
> Цей посібник передбачає знайомство з React та Next.js App Router. Приклади використовують Next.js 14+, який нативно реалізує React Server Components.
## Understanding the Server Components Architecture
Server Components (RSC) запроваджують нову парадигму: частина компонентів виконується виключно на сервері, інші — на клієнті, і обидва типи можуть співіснувати в одному дереві компонентів. Такий розподіл суттєво оптимізує продуктивність, зменшуючи обсяг JavaScript, що надсилається до браузера.
Основна ідея базується на тому, що багатьом компонентам інтерактивність не потрібна. Компонент, який відображає список статей із бази даних, може повністю виконуватися на стороні сервера. Лише інтерактивні елементи (кнопки, форми, анімації) потребують JavaScript на стороні клієнта.
```tsx
// app/articles/page.tsx
// This component runs only on the server
// No JavaScript is sent to the client for this component
import { getArticles } from '@/lib/articles'
import ArticleCard from './ArticleCard'
import LikeButton from './LikeButton'
// async/await directly in the component
// Only possible with Server Components
export default async function ArticlesPage() {
// Direct database call (no REST API needed)
const articles = await getArticles()
return (
Recent Articles
{articles.map((article) => (
// ArticleCard is also a Server Component
{/* LikeButton is a Client Component (interactive) */}
))}
)
}
```
Директива `"use client"` явно позначає компоненти, яким потрібен JavaScript у браузері.
```tsx
// app/articles/LikeButton.tsx
'use client'
// useState and interactive hooks require "use client"
import { useState, useTransition } from 'react'
import { likeArticle } from '@/actions/articles'
interface LikeButtonProps {
articleId: string
initialLikes?: number
}
export default function LikeButton({ articleId, initialLikes = 0 }: LikeButtonProps) {
// Local state for optimistic UI
const [likes, setLikes] = useState(initialLikes)
const [isPending, startTransition] = useTransition()
const handleLike = () => {
// Immediate optimistic update
setLikes((prev) => prev + 1)
// Server Action to persist
startTransition(async () => {
await likeArticle(articleId)
})
}
return (
)
}
```
Така архітектура суттєво зменшує розмір JavaScript-бандлу: на клієнт надсилається лише код `LikeButton`, а не `ArticlesPage` чи `ArticleCard`.
## Server/Client Composition Patterns
Композиція між Server Components та Client Components підпорядковується чітким правилам. Server Component може імпортувати та рендерити Client Components, але зворотне неможливе напряму. Для передачі серверного вмісту до клієнтського компонента використовується патерн `children`.
```tsx
// components/InteractiveWrapper.tsx
'use client'
import { useState, ReactNode } from 'react'
interface InteractiveWrapperProps {
children: ReactNode
expandable?: boolean
}
// Client Component that wraps server content
export function InteractiveWrapper({ children, expandable = false }: InteractiveWrapperProps) {
const [isExpanded, setIsExpanded] = useState(!expandable)
if (!expandable) {
return
{children}
}
return (
{isExpanded && (
{/* children can contain Server Components */}
{children}
)}
)
}
```
```tsx
// app/dashboard/page.tsx
// Server Component using the client wrapper
import { InteractiveWrapper } from '@/components/InteractiveWrapper'
import { getStats, getRecentActivity } from '@/lib/dashboard'
export default async function DashboardPage() {
// Parallel server-side requests
const [stats, activity] = await Promise.all([
getStats(),
getRecentActivity()
])
return (
{/* Stats in an expandable wrapper */}
{/* This content is rendered server-side then passed to client */}
{/* Recent activity */}
)
}
```
Цей патерн дозволяє поєднувати клієнтську інтерактивність із серверно відрендереними даними без дублювання логіки.
## Data Fetching and Caching
> **Розширений fetch у React**
>
> React 19 автоматично розширює нативний API `fetch`, додаючи дедуплікацію та кешування. Ідентичні запити в межах одного рендеру виконуються лише один раз.
Отримання даних у Server Components відбувається безпосередньо через `async/await`. React автоматично забезпечує дедуплікацію ідентичних запитів.
```tsx
// lib/api.ts
// Centralized request configuration with caching
const API_BASE = process.env.API_URL
// Request with time-based revalidation
export async function getProducts() {
const response = await fetch(`${API_BASE}/products`, {
// Revalidate every hour
next: { revalidate: 3600 }
})
if (!response.ok) {
throw new Error('Failed to fetch products')
}
return response.json()
}
// Request without cache (real-time data)
export async function getCurrentUser() {
const response = await fetch(`${API_BASE}/me`, {
// No cache, always fresh
cache: 'no-store'
})
if (!response.ok) {
return null
}
return response.json()
}
// Request with tag for targeted invalidation
export async function getProduct(id: string) {
const response = await fetch(`${API_BASE}/products/${id}`, {
next: {
tags: [`product-${id}`],
revalidate: 3600
}
})
if (!response.ok) {
throw new Error('Product not found')
}
return response.json()
}
```
Для прямого доступу до бази даних (Prisma, Drizzle) React cache з `unstable_cache` пропонує аналогічні можливості.
```tsx
// lib/db-queries.ts
import { unstable_cache } from 'next/cache'
import { prisma } from '@/lib/prisma'
// Cache categories (rarely modified)
export const getCategories = unstable_cache(
async () => {
return prisma.category.findMany({
orderBy: { name: 'asc' }
})
},
['categories'], // Cache key
{
revalidate: 86400, // 24 hours
tags: ['categories']
}
)
// Cache products by category
export const getProductsByCategory = unstable_cache(
async (categoryId: string) => {
return prisma.product.findMany({
where: { categoryId },
include: { images: true },
orderBy: { createdAt: 'desc' }
})
},
['products-by-category'],
{
revalidate: 3600,
tags: ['products']
}
)
// Cache invalidation after mutation
export async function createProduct(data: ProductInput) {
const product = await prisma.product.create({ data })
// Invalidate related caches
revalidateTag('products')
return product
}
```
## Streaming and Suspense for Optimal UX
Стрімінг дозволяє поступово надсилати HTML до браузера, миттєво відображаючи доступні частини, поки решта завантажується. У поєднанні з Suspense цей механізм кардинально покращує Time to First Byte (TTFB) та сприйняття користувацького досвіду.
```tsx
// app/product/[id]/page.tsx
import { Suspense } from 'react'
import { getProduct } from '@/lib/products'
import ProductDetails from './ProductDetails'
import ProductReviews from './ProductReviews'
import RecommendedProducts from './RecommendedProducts'
import { Skeleton } from '@/components/ui/Skeleton'
interface ProductPageProps {
params: { id: string }
}
export default async function ProductPage({ params }: ProductPageProps) {
// This request blocks initial render
const product = await getProduct(params.id)
return (
{/* Immediate render with product data */}
{/* Reviews load via streaming */}
Customer Reviews
}>
{/* This async component will be streamed */}
{/* Recommendations too */}
Similar Products
}>
)
}
// Skeleton for reviews
function ReviewsSkeleton() {
return (
{[1, 2, 3].map((i) => (
))}
)
}
```
```tsx
// app/product/[id]/ProductReviews.tsx
// Async Server Component that will be streamed
import { getProductReviews } from '@/lib/reviews'
interface ProductReviewsProps {
productId: string
}
export default async function ProductReviews({ productId }: ProductReviewsProps) {
// This request may take time
// Component will be streamed when complete
const reviews = await getProductReviews(productId)
if (reviews.length === 0) {
return (
No reviews yet. Be the first to share your thoughts!
)
}
```
Браузер спочатку отримує HTML-оболонку зі скелетонами, а потім реальний вміст поступово впроваджується через стрімінг.
## Server Actions for Mutations
Server Actions дозволяють виконувати серверний код із клієнтських компонентів без створення API-маршрутів. Такий підхід суттєво спрощує обробку мутацій.
```tsx
// actions/cart.ts
'use server'
import { revalidatePath } from 'next/cache'
import { cookies } from 'next/headers'
import { prisma } from '@/lib/prisma'
import { getCurrentUser } from '@/lib/auth'
// Action to add to cart
export async function addToCart(productId: string, quantity: number = 1) {
const user = await getCurrentUser()
if (!user) {
// Return structured error
return { error: 'Login required', code: 'UNAUTHORIZED' }
}
try {
// Check stock
const product = await prisma.product.findUnique({
where: { id: productId }
})
if (!product || product.stock < quantity) {
return { error: 'Insufficient stock', code: 'OUT_OF_STOCK' }
}
// Add or update cart item
await prisma.cartItem.upsert({
where: {
cartId_productId: {
cartId: user.cartId,
productId
}
},
update: {
quantity: { increment: quantity }
},
create: {
cartId: user.cartId,
productId,
quantity
}
})
// Invalidate cart page cache
revalidatePath('/cart')
return { success: true, message: 'Product added to cart' }
} catch (error) {
console.error('Add to cart error:', error)
return { error: 'An error occurred', code: 'SERVER_ERROR' }
}
}
// Action to remove from cart
export async function removeFromCart(itemId: string) {
const user = await getCurrentUser()
if (!user) {
return { error: 'Login required' }
}
await prisma.cartItem.delete({
where: { id: itemId, cart: { userId: user.id } }
})
revalidatePath('/cart')
return { success: true }
}
```
```tsx
// components/AddToCartButton.tsx
'use client'
import { useTransition } from 'react'
import { addToCart } from '@/actions/cart'
import { toast } from '@/components/ui/toast'
interface AddToCartButtonProps {
productId: string
}
export function AddToCartButton({ productId }: AddToCartButtonProps) {
const [isPending, startTransition] = useTransition()
const handleClick = () => {
startTransition(async () => {
const result = await addToCart(productId)
if (result.error) {
toast.error(result.error)
return
}
toast.success(result.message)
})
}
return (
)
}
```
> **Серверна валідація**
>
> Дані в Server Actions завжди мають валідуватися на стороні сервера. Клієнтські валідації можна обійти. Для надійної валідації слід використовувати Zod або аналогічну бібліотеку.
## Error Handling and Error Boundaries
React 19 вдосконалює обробку помилок у Server Components. Error Boundaries працюють так само, як і в Client Components.
```tsx
// app/products/error.tsx
'use client'
// Error Boundary for /products segment
interface ErrorProps {
error: Error & { digest?: string }
reset: () => void
}
export default function ProductsError({ error, reset }: ErrorProps) {
return (
Loading Error
Unable to load products. Please try again.
{/* Display digest for debugging */}
{error.digest && (
Reference: {error.digest}
)}
)
}
```
```tsx
// app/products/loading.tsx
// Loading UI during initial load
export default function ProductsLoading() {
return (
{[1, 2, 3, 4, 5, 6].map((i) => (
))}
)
}
```
Для більш детальної обробки помилок у конкретних компонентах можна безпосередньо використовувати компонент `ErrorBoundary`.
```tsx
// components/ErrorBoundary.tsx
'use client'
import { Component, ReactNode } from 'react'
interface Props {
children: ReactNode
fallback?: ReactNode
}
interface State {
hasError: boolean
error?: Error
}
export class ErrorBoundary extends Component {
constructor(props: Props) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error): State {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// Log error to monitoring service
console.error('ErrorBoundary caught:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return this.props.fallback || (
An error occurred
)
}
return this.props.children
}
}
```
## Production Performance Optimizations
Для оптимізації продуктивності Server Components у продакшені існує низка технік.
```tsx
// app/layout.tsx
import { Suspense } from 'react'
import { headers } from 'next/headers'
// Preload critical data
export const dynamic = 'force-dynamic'
export default async function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
{/* Header streamed independently */}
}>
{children}
{/* Footer can be static */}
)
}
```
```tsx
// lib/prefetch.ts
// Data prefetching for links
import { preload } from 'react-dom'
export function prefetchProduct(productId: string) {
// Prefetch images
preload(`/api/products/${productId}/image`, { as: 'image' })
}
// Usage in a component
// prefetchProduct(id)}>
```
```tsx
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// Build optimizations
experimental: {
// Enable Partial Prerendering (PPR)
ppr: true,
// Optimize package imports
optimizePackageImports: ['lucide-react', '@radix-ui/react-icons']
},
// Image configuration
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{ hostname: 'cdn.example.com' }
]
}
}
module.exports = nextConfig
```
## Conclusion
React 19 Server Components фундаментально змінюють підхід до побудови React-застосунків. Ключові висновки:
- ✅ **Розділення сервера та клієнта**: `"use client"` використовується лише для інтерактивних компонентів
- ✅ **Пряме отримання даних**: `async/await` у компонентах, без потреби в useEffect чи API-маршрутах
- ✅ **Стрімінг із Suspense**: поступове відображення для кращого сприйняття UX
- ✅ **Server Actions**: спрощені мутації без створення API-ендпоінтів
- ✅ **Розумне кешування**: `revalidate` та `tags` для оптимізації продуктивності
- ✅ **Гнучка композиція**: патерн `children` для поєднання Server та Client Components
Ця архітектура дозволяє створювати продуктивніші застосунки з меншим обсягом клієнтського JavaScript, водночас суттєво спрощуючи код. Перехід на Server Components — це інвестиція, що швидко окупається з точки зору продуктивності та зручності підтримки.
---
Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack.
HTML version of this page: https://sharpskill.dev/uk/blog/react-next/react-19-server-components-production