Server Action — optimistic update, revalidatePath, revalidateTag
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'yicharevalidateTag— 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/catchichida ishlatilmaydi. U xato tashlab ishlaydi —catchuni ushlab qoladi.try/catchtashqarisida 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.