Server Action — optimistic update, revalidatePath, revalidateTag

~20 daq · Next.js · Ma'lumot Olish

Muammodan boshlaylik

Siz Twitter/X kabi ilovada "like" tugmasi yozdingiz. Foydalanuvchi bosadi — Server Action ishlaydi — javob keladi — UI yangilanadi. Bu 300-500ms oladi.

Foydalanuvchi tugmani bosdi, lekin hech narsa bo'lmadi. Yana bosdi. Yana. Javob kelganda 3 ta like qo'shildi.

Yaxshi ilovalar boshqacha ishlaydi — Instagram, Twitter da like bosganda darhol yurak to'ladi. Server javobini kutmaydi. Agar xato bo'lsa — orqaga qaytadi.

Bu — Optimistic Update.


useOptimistic — darhol UI yangilash

React 19 da useOptimistic hook — Server Action javobini kutmasdan UI ni yangilash:

// app/posts/[id]/LikeButton.tsx
'use client'

import { useOptimistic, useTransition } from 'react'
import { toggleLike } from '@/app/actions/posts'

export function LikeButton({
  postId,
  initialLikes,
  initialLiked,
}: {
  postId: number
  initialLikes: number
  initialLiked: boolean
}) {
  const [isPending, startTransition] = useTransition()

  // optimisticState — hozirgi "taxminiy" holat
  const [optimisticLiked, setOptimisticLiked] = useOptimistic(initialLiked)
  const [optimisticCount, setOptimisticCount] = useOptimistic(initialLikes)

  function handleClick() {
    startTransition(async () => {
      // 1. Darhol UI yangilanadi (server javobini kutmasdan)
      setOptimisticLiked(!optimisticLiked)
      setOptimisticCount(optimisticLiked ? optimisticCount - 1 : optimisticCount + 1)

      // 2. Server Action ishlaydi
      await toggleLike(postId)
      // Xato bo'lsa — optimisticLiked avtomatik initialLiked ga qaytadi
    })
  }

  return (
    <button onClick={handleClick} disabled={isPending}>
      {optimisticLiked ? '❤️' : '🤍'} {optimisticCount}
    </button>
  )
}
// app/actions/posts.ts
'use server'

import { revalidatePath } from 'next/cache'

export async function toggleLike(postId: number) {
  await db.toggleLike(postId)
  revalidatePath(`/posts/${postId}`)
}

Foydalanuvchi bosadi — yurak darhol to'ladi. Server 300ms da javob beradi — hech narsa o'zgarmaydi (allaqachon to'g'ri). Xato bo'lsa — yurak bo'sh holatga qaytadi.


revalidatePath — sahifani yangilash

revalidatePath — belgilangan sahifaning keshini o'chiradi. Keyingi so'rovda sahifa qayta render qilinadi.

'use server'

import { revalidatePath } from 'next/cache'

export async function addComment(postId: number, formData: FormData) {
  const text = formData.get('text') as string
  await db.insert(comments).values({ postId, text })

  // Faqat shu post sahifasini yangilash
  revalidatePath(`/posts/${postId}`)
}

export async function deletePost(id: number) {
  await db.delete(posts).where(eq(posts.id, id))

  // Barcha /posts/* sahifalarini yangilash
  revalidatePath('/posts', 'page')

  // Butun layoutni yangilash (layout da ham post ma'lumoti bo'lsa)
  revalidatePath('/posts', 'layout')
}

revalidatePath ning ikkinchi argumenti:

  • 'page' — faqat sahifalar (default)
  • 'layout' — layout va uning barcha farzandlari

revalidateTag — teglar orqali yangilash

Ko'p sahifada bir xil ma'lumot bo'lsa — teglar bilan yangilash qulay:

// Ma'lumot olishda teg qo'shish
const posts = await fetch('/api/posts', {
  next: { tags: ['posts'] }
}).then(r => r.json())

const post = await fetch(`/api/posts/${id}`, {
  next: { tags: ['posts', `post-${id}`] }
}).then(r => r.json())
// actions/posts.ts
'use server'

import { revalidateTag } from 'next/cache'

export async function publishPost(formData: FormData) {
  await db.insert(posts).values(...)

  // 'posts' tegli barcha fetch lar yangilanadi
  // → /posts sahifasi, /posts/[slug] sahifasi, sidebar dagi so'nggi postlar...
  revalidateTag('posts')
}

export async function updatePost(id: number, formData: FormData) {
  await db.update(posts).set(...).where(eq(posts.id, id))

  revalidateTag(`post-${id}`)  // faqat shu post yangilanadi
}

revalidatePath vs revalidateTag:

  • revalidatePath — aniq URL bo'yicha
  • revalidateTag — teg bo'yicha, bir nechta sahifani bir vaqtda yangilash uchun

redirect — Server Action dan yo'naltirish

Server Action muvaffaqiyatli tugagandan keyin boshqa sahifaga yo'naltirish:

// actions/auth.ts
'use server'

import { redirect } from 'next/navigation'

export async function login(formData: FormData) {
  const email    = formData.get('email') as string
  const password = formData.get('password') as string

  const user = await db.getUserByEmail(email)

  if (!user || !verifyPassword(password, user.passwordHash)) {
    return { error: 'Email yoki parol noto\'g\'ri' }
  }

  await createSession(user.id)
  redirect('/dashboard')  // ← muvaffaqiyatli — dashboard ga
}

⚠️ redirect — try/catch ichida ishlatilmaydi. U xato tashlab ishlaydi — catch uni ushlab qoladi. try/catch tashqarisida chaqiring.

// ❌
try {
  await createSession(user.id)
  redirect('/dashboard')  // catch ushlaydi!
} catch (e) {
  return { error: 'Xato' }
}

// ✅
await createSession(user.id)
redirect('/dashboard')  // try/catch tashqarisida

Tez-tez uchraydigan xatolar

1. useOptimistic ni startTransition tashqarisida chaqirish

// ❌ — startTransition tashqarisida
function handleClick() {
  setOptimisticLiked(!optimisticLiked)  // xato!
  startTransition(async () => {
    await toggleLike(postId)
  })
}

// ✅ — startTransition ichida
function handleClick() {
  startTransition(async () => {
    setOptimisticLiked(!optimisticLiked)
    await toggleLike(postId)
  })
}

2. redirect ni try/catch ichida ishlatish

Yuqorida ko'rsatildi — redirect xato tashlab ishlaydi, catch uni ushlaydi.

3. revalidatePath va revalidateTag ni birga haddan ko'p ishlatish

// ❌ — keraksiz ko'p yangilash, server yuki oshadi
revalidatePath('/')
revalidatePath('/posts')
revalidatePath('/posts/[slug]')
revalidateTag('posts')
revalidateTag('home')

// ✅ — faqat kerakli
revalidateTag('posts')  // bir teg — barcha bog'liq sahifalar

Xulosa

Optimistic Update — Server Action javobini kutmasdan UI yangilash. Xato bo'lsa — orqaga qaytadi.

useOptimistic      → darhol UI yangilash, startTransition ichida
revalidatePath     → aniq URL keshini o'chirish
revalidateTag      → teg bo'yicha bir nechta sahifani yangilash
redirect           → muvaffaqiyatli tugagandan keyin yo'naltirish
                     try/catch tashqarisida ishlatiladi

Keyingi dars

24-dars: Dynamic Route — [id], [...slug], [[...slug]] — farqlari

To'rtinchi bo'lim — Routing Chuqurroq. Dynamic route larning barcha turlarini ko'ramiz.