Server va Client Component birga ishlatish — composition pattern

~20 daq · Next.js · Rendering Strategiyalari

Muammodan boshlaylik

Oldingi ikki darsda Server va Client Component ni alohida o'rgandik. Lekin haqiqiy loyihada ular doim birga ishlaydi.

Tasavvur qiling — e-commerce saytning mahsulot sahifasi:

┌─────────────────────────────────────┐
│  Mahsulot nomi, narxi, tavsifi      │  ← statik, serverdan keladi
│  Rasm galereyasi                    │  ← slayder, brauzerda ishlaydi
│  "Savatga qo'shish" tugmasi         │  ← onClick, brauzerda ishlaydi
│  O'xshash mahsulotlar               │  ← statik, serverdan keladi
└─────────────────────────────────────┘

Bir sahifada ham Server, ham Client Component kerak. Ularni qanday to'g'ri tashkil qilish — bu darsning mavzusi.


Asosiy qoida — daraxt

Server va Client Component larni daraxt sifatida tasavvur qiling. Daraxtning ildizi — Server Component. Barglar — Client Component.

Page (Server)
├── ProductInfo (Server)     ← DB dan ma'lumot
├── ImageGallery (Client)    ← slayder, onClick
├── AddToCart (Client)       ← savat, useState
└── RelatedProducts (Server) ← DB dan ma'lumot

Server Component Client Component ni import qila oladi — bu ishlaydi. Client Component Server Component ni import qila olmaydi — bu ishlamaydi.


To'g'ri tashkil qilish — amaliy misol

Mahsulot sahifasini quramiz:

// app/products/[id]/page.tsx — Server Component
import { ImageGallery } from '@/components/ImageGallery'
import { AddToCart } from '@/components/AddToCart'

export default async function ProductPage({ params }) {
  const { id } = await params
  const product = await db.getProduct(id)  // serverda

  return (
    <div>
      {/* Statik kontent — serverda render */}
      <h1>{product.name}</h1>
      <p>{product.price} so'm</p>
      <p>{product.description}</p>

      {/* Interaktiv qismlar — Client Component */}
      <ImageGallery images={product.images} />
      <AddToCart productId={product.id} price={product.price} />
    </div>
  )
}
// components/ImageGallery.tsx — Client Component
'use client'

export function ImageGallery({ images }: { images: string[] }) {
  const [current, setCurrent] = useState(0)

  return (
    <div>
      <img src={images[current]} alt="Mahsulot" />
      <div>
        {images.map((_, i) => (
          <button key={i} onClick={() => setCurrent(i)}>
            {i + 1}
          </button>
        ))}
      </div>
    </div>
  )
}
// components/AddToCart.tsx — Client Component
'use client'

export function AddToCart({ productId, price }) {
  const [added, setAdded] = useState(false)

  function handleClick() {
    // savatga qo'shish logikasi
    setAdded(true)
  }

  return (
    <button onClick={handleClick}>
      {added ? '✓ Savatda' : 'Savatga qo\'shish'}
    </button>
  )
}

product ma'lumotlari serverda olinadi. Interaktiv qismlar faqat kerakli ma'lumotni props orqali oladi.


children pattern — Client ichida Server

Ba'zan Client Component ni Server Component bilan "to'ldirish" kerak. Masalan, modal oyna — ochiq/yopiq holati Client da, lekin ichidagi kontent Server da render bo'lishi kerak.

// app/users/page.tsx — Server Component
import { Modal } from '@/components/Modal'

export default async function UsersPage() {
  const users = await db.getUsers()  // serverda

  return (
    <Modal title="Foydalanuvchilar">
      {/* Bu kontent serverda render bo'ladi */}
      <ul>
        {users.map(u => <li key={u.id}>{u.name}</li>)}
      </ul>
    </Modal>
  )
}
// components/Modal.tsx — Client Component
'use client'

export function Modal({ title, children }) {
  const [isOpen, setIsOpen] = useState(true)

  if (!isOpen) return null

  return (
    <div className="modal-overlay">
      <div className="modal">
        <div className="modal-header">
          <h2>{title}</h2>
          <button onClick={() => setIsOpen(false)}>✕</button>
        </div>
        <div className="modal-body">
          {children}  {/* Server Component shu yerda */}
        </div>
      </div>
    </div>
  )
}

children — Server Component da render bo'ladi, Modal ga tayyor HTML sifatida uzatiladi. Modal faqat ochiq/yopiq holatini boshqaradi.


Context — Client Component daraxtida

React.createContext faqat Client Component da ishlaydi. Lekin Provider ni Server Component da ishlatish mumkin — children orqali:

// components/ThemeProvider.tsx — Client Component
'use client'

import { createContext, useContext, useState } from 'react'

const ThemeContext = createContext('light')

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light')

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  )
}

export function useTheme() {
  return useContext(ThemeContext)
}
// app/layout.tsx — Server Component
import { ThemeProvider } from '@/components/ThemeProvider'

export default function RootLayout({ children }) {
  return (
    <html>
      <body>
        <ThemeProvider>
          {children}  {/* Server Component lar ham shu yerda */}
        </ThemeProvider>
      </body>
    </html>
  )
}

ThemeProvider — Client Component. Lekin children — Server Component bo'lishi mumkin. Context faqat Client Component larga ta'sir qiladi.


Noto'g'ri pattern — Server Component ni Client dan import qilish

// ❌ — bu ishlamaydi
'use client'

import { UserList } from './UserList'  // Server Component

export function Dashboard() {
  const [tab, setTab] = useState('users')
  return (
    <div>
      <button onClick={() => setTab('users')}>Foydalanuvchilar</button>
      {tab === 'users' && <UserList />}  {/* Server Component — ishlamaydi */}
    </div>
  )
}

Nima uchun ishlamaydi? Dashboard — Client Component. U brauzerda ishlaydi. UserList — Server Component, u faqat serverda ishlaydi. Brauzerda Server Component bo'lmaydi.

Yechim — UserList ni children orqali uzatish:

// app/dashboard/page.tsx — Server Component
import { Dashboard } from './Dashboard'
import { UserList } from './UserList'

export default async function Page() {
  return (
    <Dashboard userList={<UserList />} />
  )
}
// Dashboard.tsx — Client Component
'use client'

export function Dashboard({ userList }) {
  const [tab, setTab] = useState('users')
  return (
    <div>
      <button onClick={() => setTab('users')}>Foydalanuvchilar</button>
      {tab === 'users' && userList}  {/* tayyor HTML */}
    </div>
  )
}

Tez-tez uchraydigan xatolar

1. Hamma narsani Client Component qilish

// ❌ — sahifaning 90% statik, lekin hammasi Client
'use client'
export default function Page() {
  const [count, setCount] = useState(0)
  return (
    <div>
      <h1>Sarlavha</h1>        {/* statik */}
      <p>Uzoq matn...</p>      {/* statik */}
      <button onClick={...}>{count}</button>  {/* faqat shu Client kerak */}
    </div>
  )
}
// ✅ — faqat interaktiv qism Client
export default function Page() {
  return (
    <div>
      <h1>Sarlavha</h1>
      <p>Uzoq matn...</p>
      <Counter />  {/* faqat shu Client Component */}
    </div>
  )
}

2. Server Component ga serialize qilib bo'lmaydigan props uzatish

// ❌ — funksiya props orqali Server → Client uzatilmaydi
export default function Page() {
  const handleClick = () => console.log('bosildi')
  return <ClientComp onClick={handleClick} />  // xato!
}

Server Component dan Client Component ga faqat serialize qilinadigan qiymatlar uzatiladi: string, number, array, object, JSX. Funksiya uzatilmaydi.

// ✅ — funksiya Client Component ning o'zida bo'lishi kerak
// ClientComp.tsx
'use client'
export function ClientComp() {
  const handleClick = () => console.log('bosildi')
  return <button onClick={handleClick}>Bosing</button>
}

Xulosa

Composition pattern — Server va Client Component larni to'g'ri tashkil qilish.

Server Component  → ma'lumot olish, statik kontent
Client Component  → interaktivlik, holat, brauzer API

Server → Client: props orqali ma'lumot uzatish ✅
Client → Server: to'g'ridan-to'g'ri import ❌, children orqali ✅

Asosiy qoida: daraxtning ildizi Server Component, barglar Client Component. 'use client' ni imkon qadar pastga suring.


Keyingi dars

13-dars: Static Rendering — build vaqtida generatsiya, qachon mos

Rendering strategiyalarining birinchisi — Static Rendering. Qachon ishlaydi, qanday sozlanadi, qachon mos emas.