Mundarija (33)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi optimistik qiymat
- 2.1 "Yoqdi" — darhol
- 2.2 Nega bu oldingi darsdagi qoidaga zid emas
- 3. Rad etilganda — o'zi qaytadi
- 3.1 Server "yo'q" dedi
- 3.2 catch ham Action'ni tugatishi kerak
- 3.3 Transition'siz — ogohlantirish
- 4. Reducer bilan: ro'yxatga qo'shish
- 4.1 Chat: "yuborilmoqda"
- 4.2 Bir necha kutilayotgan o'zgarish
- 5. Poyga: qo'lda qaytarish va useOptimistic
- 5.1 Qo'lda — "oldingi qiymat"
- 5.2 useOptimistic bilan
- 6. Qachon optimistik, qachon yo'q
- 7. Vazifalar qadami: optimistik yozuvlar va commit (3-qism)
- 7.1 Ekrandagi ro'yxat — optimistik
- 7.2 yozuv — Action'ning yuragi
- 7.3 Katak bosish
- 7.4 Forma va e'lon
- 7.5 Nima optimistik emas
- 7.6 Fokus — rejalashtirilgan
- 7.7 Testlar, brauzer va commit
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): ekranda nima?
- 2-mashq (o'rta): obuna tugmasi
- 3-mashq (qiyin): sharhlar — "yuborilmoqda" va rad etilish
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
useOptimistic: darhol javob beradigan UI — optimistik state va avtomatik qaytarish
Qisqacha:
useOptimistic(value)— "server tasdiqlaydi" deb oldindan ko'rsatiladigan qiymat.const [optimistic, setOptimistic] = useOptimistic(value): Action ichidasetOptimistic(...)chaqirsangiz, ekranda darhol yangi qiymat chiqadi. Action tugaganda React optimistik qatlamni o'zi olib tashlaydi va ekranda yanavalue— haqiqiy state qoladi. Server tasdiqlagan bo'lsa, sizvalueni yangilagansiz va farq ko'rinmaydi. Rad etgan bo'lsa — ekran o'zi eski holatga qaytadi, "oldingi qiymatni qaytarish" kodi kerak emas. Ikkinchi argument — reducer:useOptimistic(list, (list, newItem) => [...list, newItem]).setOptimisticfaqat Action yoki transition ichida ishlaydi.
Bu darsda
- "Yoqdi" tugmasini server javobini kutmasdan ishlatasiz va rad etilganda qaytishini jonli ko'rasiz.
- Optimistik qatlam ichida nima bo'lishini (asosiy state + kutilayotgan o'zgarishlar) tushuntira olasiz.
- Reducer bilan chat ro'yxatiga "yuborilmoqda" xabarini qo'shasiz.
- Qo'lda qaytarish (v7 usuli) va
useOptimisticni ikki yozuv poygasida solishtirasiz. vazifalardagi katak, tahrir, o'chirish va formani optimistik qilib, uch darslik qadamni commit qilasiz.
Oldin bilishingiz kerak: Actions, useActionState va useFormStatus, useReducer, State II: snapshot.
1. Nega bu kerak?
Instagram'da yurakchani bosasiz — u darhol qizil bo'ladi. Telefonda internet sekin, so'rov hali serverga yetmagan. Lekin ilova "server rozi bo'ladi" deb ishonadi va natijani oldindan ko'rsatadi. Bu — optimistik yangilash (optimistic update): server javobini kutmasdan, muvaffaqiyatni taxmin qilib ekranni yangilash.
Oldingi ikki darsda qarama-qarshi yo'lni ko'rdik: Action tugaguncha "Saqlanmoqda…", natija esa server javobidan keyin. Bu to'g'ri, lekin sekin internetda zerikarli. «Bahor» menyusida har "yoqdi" bosilganda yarim soniya kutish — Jasur aka "sayt qotib qoldi" deb qo'ng'iroq qiladi.
Optimistik yangilashning qiyin joyi — xato. Server rad etsa, ekranni qaytarish kerak. Sardor buni qo'lda yozdi: "oldingi qiymatni eslab qol, xato bo'lsa — qaytar". Bitta bosishda ishlaydi. Ikki bosish bir vaqtda bo'lganda esa buziladi — bu darsda buni jonli ko'ramiz.
Hayotiy o'xshatish: restoranda stolni qalam bilan band qilish. Administrator daftarga qalam bilan yozadi: "19:00 — Malika, 4 kishi". Mehmon telefon orqali tasdiqlasa, qalam ustidan ruchka bilan yoziladi. Tasdiqlamasa — qalam o'chiriladi va stol yana bo'sh. Qalam — optimistik qatlam, ruchka — haqiqiy state. useOptimistic — qalamni o'zi o'chiradigan daftar.
2. Birinchi optimistik qiymat
2.1 "Yoqdi" — darhol
import { startTransition, useOptimistic, useState } from "react";
function sendLike() {
return new Promise((resolve) => setTimeout(resolve, 200));
}
export default function App() {
const [likes, setLikes] = useState(10); // server tasdiqlagan
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
console.log("render:", likes, optimisticLikes);
function handleClick() {
startTransition(async () => {
setOptimisticLikes(optimisticLikes + 1); // darhol
await sendLike();
startTransition(() => setLikes((l) => l + 1)); // tasdiq
});
}
return (
<div>
<button onClick={handleClick}>Yoqdi</button>
<p>❤ {optimisticLikes}</p>
</div>
);
}Konsolda:
render: 10 10
render: 10 11
render: 11 11Ekranda:
Yoqdi
❤ 11ekran("❤ 11") bosilgandan 60 ms keyin tekshirdi — server hali javob bermagan, lekin ekranda allaqachon 11. Konsoldagi ikki ustunni solishtiring:
| Render | likes (haqiqiy) |
optimisticLikes (ekranda) |
Nima bo'ldi |
|---|---|---|---|
| 1 | 10 | 10 | birinchi chizish |
| 2 | 10 | 11 | bosish: optimistik qatlam qo'shildi |
| 3 | 11 | 11 | server tasdiqladi, Action tugadi, qatlam olindi |
2-render — optimistik. Haqiqiy likes hali 10, ekranda esa 11. 3-renderda React qatlamni olib tashladi. Lekin likes endi 11 — ekranda farq ko'rinmaydi.
2.2 Nega bu oldingi darsdagi qoidaga zid emas
Actions darsida "Action ichidagi setterlar Action tugagach chiziladi" degan edik. setOptimisticLikes ham Action ichida — nega u darhol ko'rindi? Chunki useOptimistic aynan shu qoidaning istisnosi uchun yaratilgan. Uning setteri Action davomida chiziladi va Action tugashi bilan bekor bo'ladi. Oddiy setter — "natija", optimistik setter — "natija kelguncha ko'rsatib turiladigan taxmin".
3. Rad etilganda — o'zi qaytadi
3.1 Server "yo'q" dedi
Endi server rad etadi. Qaytarish kodini yozmaymiz:
import { startTransition, useOptimistic, useState } from "react";
function sendLike() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error("500: server xatosi")), 200);
});
}
export default function App() {
const [likes, setLikes] = useState(10);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
const [error, setError] = useState(null);
function handleClick() {
startTransition(async () => {
setOptimisticLikes(optimisticLikes + 1);
try {
await sendLike();
startTransition(() => setLikes((l) => l + 1));
} catch (err) {
startTransition(() => setError(err.message));
}
});
}
return (
<div>
<button onClick={handleClick}>Yoqdi</button>
<p>❤ {optimisticLikes}</p>
{error && <p role="alert">Saqlanmadi: {error}</p>}
</div>
);
}Ekranda:
Yoqdi
❤ 10
Saqlanmadi: 500: server xatosiBosish — darhol 11 (ekran tasdiqladi). 200 ms dan keyin server rad etdi. Action tugadi — React optimistik qatlamni oldi va ekranda likes qoldi: 10. catch da biz faqat xato matnini qo'ydik, 10 ni qaytarish haqida bir qator ham yozmadik.
stateDiagram-v2
[*] --> Haqiqiy
Haqiqiy --> Optimistik: setOptimistic (Action ichida)
Optimistik --> Tasdiqlandi: server ok, setValue
Optimistik --> Haqiqiy: server rad, Action tugadi
Tasdiqlandi --> Haqiqiy: qatlam olindi, farq yo'qDiagrammadagi ikki yo'lni solishtiring. Ikkalasining oxiri bir xil: Action tugaganda qatlam olinadi. Farq faqat shunda — siz value ni yangiladingizmi, yo'qmi.
3.2 catch ham Action'ni tugatishi kerak
try/catch nega kerak? Rad etilgan Promise'ni ushlamasak, Action throw qiladi — Actions darsidagidek, ilova yiqiladi. Kutilgan xato (server rad etdi) — qiymat: catch ichida xabar.
setError ham await dan keyin — shuning uchun u ham startTransition ichida. U Action bilan birga chiziladi: ekran "11 → 10 + xato" ni bitta kadrda ko'rsatadi, oraliq "10, xatosiz" kadr bo'lmaydi.
Tekshirib ko'ring:
try/catchni olib tashlasak, ekranda nima bo'ladi?
Javob
Action rad etilgan Promise bilan tugaydi — xato eng yaqin error boundary'ga ketadi. Error boundary bo'lmasa — "Komponentni chizishda xato" qutisi, butun ilova o'chadi. Optimistik qatlam baribir olinardi, lekin ko'radigan ekran qolmaydi. Kutilgan xatoni qiymatga aylantiring; error boundary — keyingi darsda.
3.3 Transition'siz — ogohlantirish
setOptimistic ni oddiy hodisada chaqirsa-chi?
import { useOptimistic, useState } from "react";
export default function App() {
const [likes] = useState(10);
const [optimisticLikes, setOptimisticLikes] = useOptimistic(likes);
return (
<div>
<button onClick={() => setOptimisticLikes(11)}>Yoqdi</button>
<p>❤ {optimisticLikes}</p>
</div>
);
}Konsolda:
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.Ekranda:
Yoqdi
❤ 10"Optimistik yangilash transition yoki Action'dan tashqarida bo'ldi. Tuzatish: uni Action'ga ko'chiring yoki startTransition ga o'rang." Optimistik qatlam "Action davom etayotgan paytda" yashaydi. Action yo'q — qatlamning yashash vaqti ham yo'q. React ogohlantirdi va ekran yana 10 da.
4. Reducer bilan: ro'yxatga qo'shish
4.1 Chat: "yuborilmoqda"
Ko'pincha optimistik qiymat — butun ro'yxat, o'zgarish esa — bitta element. Buning uchun useOptimistic ikkinchi argument oladi — reducer:
const [optimisticList, addOptimistic] = useOptimistic(
list, // asosiy state
(current, newItem) => [...current, newItem], // qanday qo'shiladi
);addOptimistic(x) chaqirilganda React reducer(joriy, x) ni hisoblaydi. useReducer dagi (holat, amal) => yangi holat — xuddi o'sha shakl.
«Bahor» oshxonasining ichki chati: ofitsiant oshpazga xabar yuboradi. Xabar darhol ro'yxatda chiqadi, yonida "yuborilmoqda…":
import { useOptimistic, useState } from "react";
function sendToKitchen(text) {
return new Promise((resolve) => {
setTimeout(() => resolve(text), 200);
});
}
export default function App() {
const [messages, setMessages] = useState([
{ text: "Salom, oshxona!", sending: false },
]);
const [optimisticMessages, addOptimistic] = useOptimistic(
messages,
(current, text) => [...current, { text, sending: true }],
);
async function sendAction(formData) {
const text = formData.get("text");
addOptimistic(text);
const saved = await sendToKitchen(text);
setMessages((m) => [...m, { text: saved, sending: false }]);
}
return (
<form action={sendAction}>
<ul>
{optimisticMessages.map((m, i) => (
<li key={i}>
{m.text}
{m.sending && " (yuborilmoqda…)"}
</li>
))}
</ul>
<label>
Xabar <input name="text" />
</label>
<button>Yuborish</button>
</form>
);
}Ekranda:
Salom, oshxona!
3-stolga osh
Xabar YuborishBosilgandan 60 ms keyin ekranda "3-stolga osh (yuborilmoqda…)" bor edi (ekran tekshirdi). 200 ms dan keyin xabar haqiqiy ro'yxatga yozildi va qatlam olindi — "(yuborilmoqda…)" yo'qoldi.
Ikki narsaga e'tibor bering. Birinchisi: <form action> — o'zi Action, shuning uchun startTransition shart emas. Ikkinchisi: setMessages await dan keyin, lekin startTransition siz. Bu yerda u xavfsiz — forma Action'ining oxirgi qatori. Lekin vazifalar dagi kabi og'ir ro'yxatda uni o'rash kerak (Actions). Kanon aynan shunday qiladi.
4.2 Bir necha kutilayotgan o'zgarish
Ofitsiant tez-tez uchta xabar yozdi. Qatlamda uchta kutilayotgan o'zgarish bor. React ularni tartib bilan, asosiy state ustiga qo'yadi:
ekran = reducer(reducer(reducer(asosiy, x1), x2), x3)Ikki qoida bor. Birinchisi — Actions darsidan: bir vaqtda boshlangan Action'lar birga tugaydi. Bitta xabar tasdiqlansa yoki rad etilsa ham, React hammasi tugashini kutadi va natijani bitta kadrda chizadi:
| Qadam | Asosiy | Kutilayotgan | Ekranda |
|---|---|---|---|
| 3 xabar yuborildi | [a] |
x1, x2, x3 | a x1 x2 x3 |
| x1 tasdiqlandi, x2 rad etildi, x3 yo'lda | [a] (tasdiq ham kutadi) |
x1, x2, x3 | a x1 x2 x3 |
| x3 tasdiqlandi — hammasi tugadi | [a, x1, x3] |
— | a x1 x3 |
Ikkinchisi: asosiy state Action'dan tashqarida o'zgarsa (masalan, boshqa tabdan yangi xabar keldi), React reducer'ni yangi asos ustida qayta hisoblaydi. Kutilayotganlar yo'qolmaydi — ular yangi asosga "ko'chiriladi":
import { startTransition, useOptimistic, useState } from "react";
export default function App() {
const [list, setList] = useState(["a"]);
const [shown, addOptimistic] = useOptimistic(
list,
(current, item) => [...current, `${item}?`],
);
function handleClick() {
// 100 ms da "boshqa tab"dan b keldi — oddiy yangilanish
setTimeout(() => setList((l) => [...l, "b"]), 100);
startTransition(async () => {
addOptimistic("x");
await new Promise((resolve) => setTimeout(resolve, 300));
startTransition(() => setList((l) => [...l, "x"]));
});
}
return (
<div>
<button onClick={handleClick}>Yuborish</button>
<p>{shown.join(" ")}</p>
</div>
);
}Ekranda:
Yuborish
a b xekran("a b x?") — 160 ms da: asos [a, b] bo'ldi, kutilayotgan x? uning ustiga qayta qo'llandi. b ni hech kim optimistik ro'yxatga qo'shmagan — React o'zi qayta hisobladi. Shuning uchun reducer sof funksiya bo'lishi shart: React uni istalgancha qayta chaqiradi. Bu xususiyat vazifalar da muhim bo'ladi.
5. Poyga: qo'lda qaytarish va useOptimistic
5.1 Qo'lda — "oldingi qiymat"
v7 dagi vazifalar uslubi: darhol state'ni o'zgartiramiz, xato bo'lsa — eslab qolingan oldingi ro'yxatni qaytaramiz. Ikki katak: 1-katakni server 400 ms da rad etadi, 2-katakni 800 ms da tasdiqlaydi. Ikkalasi ketma-ket bosiladi:
import { useState } from "react";
// "server": osh — 400 ms da rad, manti — 800 ms da tasdiq
function save(dish) {
return new Promise((resolve, reject) => {
if (dish === "Osh") {
setTimeout(() => reject(new Error("500")), 400);
} else {
setTimeout(resolve, 800);
}
});
}
export default function App() {
const [done, setDone] = useState({ Osh: false, Manti: false });
async function toggle(dish) {
const previous = done; // ❌ shu bosish paytidagi surat
setDone({ ...done, [dish]: !done[dish] });
try {
await save(dish);
} catch {
setDone(previous); // qaytarish
}
}
return (
<div>
{Object.keys(done).map((dish) => (
<button key={dish} onClick={() => toggle(dish)}>
[{done[dish] ? "x" : " "}] {dish}
</button>
))}
</div>
);
}Ekranda (tugmalar yonma-yon, orasida bo'sh joy yo'q):
[ ] Osh[ ] MantiIkki ekran — oraliq holatlar. Birinchisi: ikkala katak bosildi — [x] Osh [x] Manti. Ikkinchisi: osh rad etildi — ikkalasi ham bo'sh. Mantini server tasdiqlagan bo'lsa ham, u ekranda belgilanmagan qoldi.
Nega? previous — oshni bosgan paytdagi surat: { Osh: false, Manti: false }. U paytda manti hali bosilmagan edi. "Oldingi ro'yxatni qaytarish" ikkinchi bosishni ham o'chirib yubordi. Bu — State II dagi snapshot va eskirgan closure ning yozuvdagi ko'rinishi.
vazifalar v7 da aynan shu bo'lgan. Kanonni qurishda haqiqiy Chrome'da o'lchandi (07-poyga-v7.txt): -- → x- → xx → -- → -x. To'rtinchi holat — ikkinchi katak ~400 ms yo'qoldi. Keyin server bilan solishtirish uni qaytardi. Bizning kichik misolda solishtirish yo'q — manti abadiy yo'qoldi.
5.2 useOptimistic bilan
Endi asosiy state'ga faqat tasdiqlangan narsa yoziladi, ekran esa — optimistik qatlamdan:
import { startTransition, useOptimistic, useState } from "react";
function save(dish) {
return new Promise((resolve, reject) => {
if (dish === "Osh") {
setTimeout(() => reject(new Error("500")), 400);
} else {
setTimeout(resolve, 800);
}
});
}
function setDish(current, { dish, value }) {
return { ...current, [dish]: value };
}
export default function App() {
const [done, setDone] = useState({ Osh: false, Manti: false });
const [shown, setShown] = useOptimistic(done, setDish);
function toggle(dish) {
const change = { dish, value: !shown[dish] };
startTransition(async () => {
setShown(change); // ekranda — darhol
try {
await save(dish);
// asosiyga — faqat tasdiqlangani, eng so'nggi holat ustiga
startTransition(() => setDone((d) => setDish(d, change)));
} catch {
// hech narsa: qatlam Action tugagach o'zi yo'qoladi
}
});
}
return (
<div>
{Object.keys(shown).map((dish) => (
<button key={dish} onClick={() => toggle(dish)}>
[{shown[dish] ? "x" : " "}] {dish}
</button>
))}
</div>
);
}Ekranda:
[ ] Osh[x] MantiOraliq holatlar: ikkala katak bosildi — [x] Osh [x] Manti. Osh rad etildi (400 ms) — ekran o'zgarmadi: React manti Action'i ham tugashini kutyapti. Hech narsa yo'qolmadi. Manti tasdiqlangach (~960 ms) — bitta kadrda yakuniy holat: osh — yo'q, manti — bor.
Farq qayerda? Qo'lda usulda "qaytarish" — butun ro'yxatning surati. useOptimistic da esa ekran hisoblanadi: asosiy state + hali kutilayotgan o'zgarishlar. Hamma Action tugaganda qatlam bo'shaydi va asosiy state qoladi. Asosiy state'ga esa faqat tasdiqlangan manti yozilgan. Rad etilgan osh unga hech qachon tegmagan — qaytarish uchun hech narsa yo'q.
Updater funksiya setDone((d) => …) ham muhim: tasdiq eng so'nggi asosiy holat ustiga yoziladi, closure'dagi eski done ustiga emas.
vazifalar v8 dagi haqiqiy o'lchov (e2e-18, Chrome 155):
✅ rad etildi: katak o'zi qaytdi, e'lon: [false,"Saqlanmadi: Sinov uchun ataylab xato"]
ekran ketma-ketligi (1-katak, 2-katak): -- → x- → xx → -x
✅ poyga: oraliqda 2-katak hech qachon yo'qolmadi (v7 da "--" bo'lardi): true-- holati yo'qoldi. v7 da 5 ta holat edi, v8 da 4 ta.
Tekshirib ko'ring:
toggleichidachangenegastartTransitiondan oldin hisoblangan?!shown[dish]ni Action ichida yozsa nima bo'lardi?
Javob
Foydalanuvchi bosgan paytda ekranda nima ko'rgan bo'lsa — shu holat teskarisiga o'zgarishi kerak. change — "bosish paytidagi niyat": "oshni belgilangan qil". U serverga ham, optimistik qatlamga ham, tasdiqqa ham bir xil ketadi. Action ichida hisoblash ham bu misolda ishlaydi, lekin niyatni hodisa paytida qotirib qo'yish xavfsizroq. vazifalar kanoni ham shunday: "qiymat bosilgan paytda olinadi: serverga aynan shu yuboriladi".
6. Qachon optimistik, qachon yo'q
Optimistik UI — va'da. Va'dani ko'pincha bajara olsangizgina bering:
| Holat | Optimistikmi? | Nega |
|---|---|---|
| like, katak, tartibni o'zgartirish | ha | deyarli doim muvaffaqiyatli, qaytarish arzon |
| chatga xabar | ha, "yuborilmoqda" belgisi bilan | foydalanuvchi holatni ko'radi |
| to'lov, buyurtmani tasdiqlash | yo'q | "to'landi" deb ko'rsatib, keyin qaytarish — ishonchni buzadi |
| o'chirish (qaytarib bo'lmaydigan) | ehtiyot bilan | tasdiq oynasi + xato bo'lsa e'lon |
| serverda hisoblanadigan narsa (narx, chegirma) | yo'q | natijani brauzer bilmaydi |
Va yana: rad etilsa, foydalanuvchiga aytish kerak. Ekran jimgina qaytsa, u "men bosmagan ekanman" deb o'ylaydi. «Rad etilganda — o'zi qaytadi» misolidagi role="alert" — Malika ham eshitadi.
7. Vazifalar qadami: optimistik yozuvlar va commit (3-qism)
7.1 Ekrandagi ro'yxat — optimistik
App.tsx da asosiy ro'yxat endi asosiy deb ataladi, vazifalar esa — ekrandagi, optimistik ro'yxat:
const { vazifalar: asosiy, tahrirId } = holat;
// useOptimistic (18/#07): ekrandagi ro'yxat = asosiy (server
// tasdiqlagan) + hali kutilayotgan yozuvlar. Action tugagach
// optimistik qatlam olinadi: tasdiqlangan bo'lsa — asosiyda
// allaqachon bor, rad etilgan bo'lsa — o'z-o'zidan yo'qoladi
// (qaytarish avtomatik)
const [vazifalar, optimistik] = useOptimistic(
asosiy,
optimistikOzgartir,
);Reducer — o'sha sof vazifalarReducer (useReducer):
// useOptimistic'ning yangilash funksiyasi (18/#07): asosiy ro'yxat
// + kutilayotgan amal → ekrandagi ro'yxat. O'sha sof reducer
// (tahrir bu yerda yo'q — u optimistik emas, asosiy holatda)
function optimistikOzgartir(
vazifalar: readonly VazifaMalumoti[],
amal: VazifaAmali,
): readonly VazifaMalumoti[] {
return vazifalarReducer(boshlangichHolat(vazifalar), amal).vazifalar;
}Kanondagi izohlar bu va keyingi bloklarda qatorlarga qayta bo'lindi (telefon uchun), kod esa aynan. Yangi kod deyarli yo'q: reducer 17-qismdan beri bor edi. Endi u ikki joyda ishlaydi — asosiy holatda (tasdiq) va optimistik qatlamda (taxmin). Sof funksiya bo'lgani uchun bu xavfsiz.
7.2 yozuv — Action'ning yuragi
Actions darsida fondaYoz ni ko'rgandik: u yozuv ni startTransition(async …) ichida chaqiradi. Mana yozuv (kanondan aynan):
// Optimistik yozuv: avval ekranda (useOptimistic), keyin
// serverda. TRANSITION ICHIDA chaqiriladi (useOptimistic talabi):
// forma action'i — React o'zi, qolganlari — fondaYoz(). Rad
// etilsa — e'lon; ekrandagi o'zgarish action tugagach o'zi qaytadi
async function yozuv(
amal: VazifaAmali,
serverda: () => Promise<VazifaAmali>,
): Promise<boolean> {
optimistik(amal);
const natija = await yozuvniYubor(
serverda,
s,
() => songgiAsosiy.current,
);
if (!natija.ok) {
elonQil(natija.xabar);
}
return natija.ok;
}Uch qadam: optimistik(amal) — ekranda darhol; yozuvniYubor — server va tasdiq (startTransition ichida dispatch, Actions darsidan); rad etilsa — faqat e'lon. "Oldingi ro'yxatni qaytarish" yo'q.
songgiAsosiy — await dan keyin kerak bo'ladigan eng so'nggi asosiy ro'yxat:
// Asosiy ro'yxat — await'dan keyin (server javobi kelganda) eng
// so'nggisi kerak: closure'dagi qiymat yozuv boshlangan paytniki
const songgiAsosiy = useRef(asosiy);
useLayoutEffect(() => {
songgiAsosiy.current = asosiy;
});Bu — «Poyga» bo'limidagi setDone((d) => …) ning katta ilovadagi ko'rinishi: closure'dagi surat eskiradi, ref esa har commit'da yangilanadi (useRef I).
7.3 Katak bosish
// Qiymat bosilgan paytda olinadi: serverga aynan shu yuboriladi.
// Optimistik amal — "serverdanKeldi" (yangi qiymat bilan): ikki
// marta qo'llansa ham natija o'sha ("almashtir" ikki marta —
// teskarisi)
function katakniAlmashtir(id: number) {
elonQil("");
dispatch({ tur: "tahrirYopildi" });
const vazifa = vazifalar.find((v) => v.id === id);
if (vazifa === undefined || !onlaynmi()) {
return;
}
const yangi = {
id,
matn: vazifa.matn,
bajarildi: !vazifa.bajarildi,
};
fokusniRejala(id, "almashtir", qoshniId(korinadigan, id));
fondaYoz({ tur: "serverdanKeldi", vazifa: yangi }, () =>
serverVazifasi(api.yangila(id, { bajarildi: yangi.bajarildi })),
);
}Eng nozik joy — izohdagi gap. Optimistik amal "almashtirildi" emas, "serverdanKeldi" — tayyor yangi qiymat bilan. Nega? «Bir necha kutilayotgan o'zgarish» bo'limidagi qoida: asosiy state o'zgarsa, React kutilayotgan amallarni qayta qo'llaydi. "Almashtir" ikki marta qo'llansa — teskarisi bo'ladi: katak o'z-o'zidan qaytib qoladi. "Shu qiymat bo'lsin" esa necha marta qo'llansa ham bir xil — bunday amal idempotent deyiladi (Pure komponentlar). Reducer'ga yangi case qo'shildi:
case "serverdanKeldi": {
const { vazifa } = amal;
const bor = holat.vazifalar.some((v) => v.id === vazifa.id);
return {
...holat,
vazifalar: bor
? holat.vazifalar.map((v) =>
v.id === vazifa.id ? vazifa : v,
)
: [...holat.vazifalar, vazifa],
};
}Bor bo'lsa — almashtiradi, yo'q bo'lsa — oxiriga qo'shadi. Server bergan yangi vazifa (yangi id bilan) ham shu yo'ldan keladi — serverVazifasiniTekshir javobni tekshiradi va VazifaMalumoti ga aylantiradi. Yangi vazifa id'si endi serverdan: TEXNIK-QARZ 5-band yopildi.
7.4 Forma va e'lon
Oldingi darsdagi forma onQosh ni kutadi. App tomonda — vazifaniQosh:
const korinsin = qoshilgandaKorinsin(matn, filtr, qidiruv);
const xabar = `«${matn}» qo'shildi.`;
// darhol (optimistik); tasdiqlansa — asosiyga
elonniKorsat(xabar);
const qoshildi = await yozuv({ tur: "qoshildi", matn }, () =>
serverVazifasi(api.qosh(matn)),
);
if (!qoshildi) {
return { tur: "saqlanmadi" };
}
// await'dan keyin — yana transition (action bilan birga
// tugasin)
startTransition(() => {
setQidiruv(korinsin.qidiruv);
elonQil(xabar);
});elonniKorsat — yana bir useOptimistic, bu safar e'lon matni uchun:
const [asosiyElon, elonQil] = useElon();
// Forma action'idagi oddiy yangilanishlar action TUGAGACH
// chiziladi (18/#05); «qo'shildi» e'loni esa darhol kerak — u ham
// optimistik
const [elon, elonniKorsat] = useOptimistic(asosiyElon);Bu — Actions dagi "Saqlanmoqda… ko'rinmadi" tuzog'ining yechimi. Oddiy elonQil(xabar) forma Action'i ichida server javobigacha kutardi. elonniKorsat esa darhol ko'rinadi, tasdiqdan keyin elonQil uni haqiqiy qiladi.
7.5 Nima optimistik emas
Ikki joy ataylab asosiy ro'yxatni ishlatadi:
localStorage—saqla(asosiy): diskka faqat server tasdiqlagani yoziladi. Sahifa yangilansa, rad etilgan o'zgarish qaytib kelmaydi. e2e-18:kesh hali eski (faqat tasdiqlangan saqlanadi): "1- 2- 3x".- Eksport —
eksportMatni(asosiy): faylga taxmin emas, haqiqat.
Boshqa tablarga ham faqat tasdiq yuboriladi (yozuvniYubor ichida tablargaYubor(oxirgi)).
7.6 Fokus — rejalashtirilgan
v7 da fokus flushSync dan keyin darhol qo'yilardi. Optimistik yangilanish esa transition ichida — uni flushSync bilan chizib bo'lmaydi. Kanon fokusni rejalashtiradi:
function fokusniRejala(
id: number | null,
amal: FokusAmali,
zaxira: number | null,
) {
fokusRejasi.current = { id, amal, zaxira };
}
useLayoutEffect(() => {
const reja = fokusRejasi.current;
if (reja !== null) {
fokusRejasi.current = null;
fokusla(reja.id, reja.amal, reja.zaxira);
}
});Reja — ref'da (render chaqirmaydi). Keyingi commit'dan keyin useLayoutEffect uni bajaradi — bo'yashdan oldin. Katak bosilib, saralash qatorni boshqa joyga ko'chirsa, React <li> ni ko'chiradi va fokus tushib qoladi. Shu effekt uni qaytaradi. TEXNIK-QARZ 33 qisman to'landi.
7.7 Testlar, brauzer va commit
Uch darsdagi ish — bitta commit:
git add -A
git commitCommit (kanon): refactor: yozuvlar — action va useOptimistic; forma — useActionState. Body: forma — <form action> + useActionState({xato, matn}) + useFormStatus (tugma "Qo'shilmoqda…"); App — useOptimistic(asosiy, reducer), e'lon ham optimistik; yozuvniYubor — tasdiq startTransition ichida asosiy holatga (server javobi: serverdanKeldi, server id); fokus — fokusniRejala + useLayoutEffect.
Tekshiruvlar (haqiqiy, tekshiruv-07.txt):
npm test— node:testℹ tests 163(+3:serverdanKeldi,serverVazifasiniTekshir), VitestTests 33 passed (33).lint,format:check,tip—exit 0. Build:index-….js 254.86 kB │ gzip: 80.76 kB.- Brauzer — 159/159, birinchi urinishda, ssenariy o'zgarishsiz. e2e-18 — 22/22.
- Diff: 8 fayl, +431 −136.
Server 500 qaytarganda optimistik katak ekranda qancha turadi? O'lchandi (10-optimistik-vaqt.txt, haqiqiy «Bahor» serveri): 17–29 ms. Ko'z buni zo'rg'a sezadi — e'lon esa qoladi. v7 da ham xuddi shuncha edi.
8. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
An optimistic state update occurred outside a transition or action |
setOptimistic oddiy hodisada |
startTransition yoki forma Action'i ichida |
| optimistik qiymat bir lahza ko'rinib, darhol yo'qoladi | Action tugadi, lekin asosiy state yangilanmadi | tasdiqda setValue ni unutmang |
| ikki bosishda biri yo'qoladi | qo'lda "oldingi qiymatni qaytarish" | useOptimistic + asosiyga faqat tasdiq |
| katak o'z-o'zidan teskarisiga qaytdi | optimistik amal "almashtir" — qayta qo'llanganda teskari | "shu qiymat bo'lsin" (idempotent) amal |
| rad etilganda oq ekran | await dagi xato ushlanmagan |
try/catch → e'lon |
| sahifa yangilansa rad etilgan o'zgarish qaytdi | localStorage ga optimistik ro'yxat yozilgan |
saqlashda — faqat asosiy state |
9. Mashqlar
Mashqlar kurs/mashqlar/18/07-useoptimistic/ da. Har yechimni «Ishga tushir» bilan sinang.
1-mashq (oson): ekranda nima?
likes — 5. Foydalanuvchi «Yoqdi» ni bosdi: Action ichida setOptimisticLikes(6), server 300 ms dan keyin rad etdi, catch da faqat xato matni qo'yildi.
- Bosilgandan 100 ms keyin ekranda:
- 500 ms keyin ekranda:
- 500 ms keyin
likes(asosiy state):
Yechim
- 6 — optimistik qatlam Action davomida ko'rinadi.
- 5 — Action tugadi, qatlam olindi; asosiy state o'zgarmagan.
- 5 — rad etilgan yozuv asosiy state'ga hech qachon tegmagan. Shuning uchun qaytarish kodi kerak emas.
2-mashq (o'rta): obuna tugmasi
«Bahor» Telegram kanaliga obuna tugmasi. Hozir u server javobini (300 ms) kutadi. useOptimistic bilan tugma matnini darhol "Obunasiz" → "Obuna bo'ldingiz" ga o'zgartiring. Server tasdiqlagach — asosiy state ham yangilansin. Ishora: «"Yoqdi" — darhol» bo'limi; await dan keyingi setter — startTransition ichida.
import { startTransition, useState } from "react";
function subscribe() {
return new Promise((resolve) => setTimeout(resolve, 300));
}
export default function App() {
const [subscribed, setSubscribed] = useState(false);
function handleClick() {
startTransition(async () => {
await subscribe();
startTransition(() => setSubscribed(true));
});
}
return (
<div>
<button onClick={handleClick}>Obuna bo'lish</button>
<p>{subscribed ? "Obuna bo'ldingiz" : "Obunasiz"}</p>
</div>
);
}Ekranda (bosgandan 100 ms keyin):
Obuna bo'lish
ObunasizYechim
import { startTransition, useOptimistic, useState } from "react";
function subscribe() {
return new Promise((resolve) => setTimeout(resolve, 300));
}
export default function App() {
const [subscribed, setSubscribed] = useState(false);
const [shown, setShown] = useOptimistic(subscribed);
function handleClick() {
startTransition(async () => {
setShown(true);
await subscribe();
startTransition(() => setSubscribed(true));
});
}
return (
<div>
<button onClick={handleClick}>Obuna bo'lish</button>
<p>{shown ? "Obuna bo'ldingiz" : "Obunasiz"}</p>
</div>
);
}Ekranda (bosgandan 100 ms keyin):
Obuna bo'lish
Obuna bo'ldingizEkran endi shown dan chiziladi. Server hali javob bermagan (100 ms < 300 ms), lekin matn allaqachon yangi. 300 ms da subscribed ham true bo'ladi va qatlam olinganda farq ko'rinmaydi.
3-mashq (qiyin): sharhlar — "yuborilmoqda" va rad etilish
Taom sahifasidagi sharhlar formasi. Talablar:
- Yuborilgan sharh ro'yxatda darhol chiqsin, yonida " (yuborilmoqda…)".
postReview(o'zgartirmang) bo'sh yoki 3 harfdan qisqa sharhni rad etadi. Rad etilsa — sharh ro'yxatdan o'zi yo'qolsin varole="alert"da "Saqlanmadi: …" chiqsin.- Tasdiqlangan sharh asosiy ro'yxatga yozilsin (" (yuborilmoqda…)" siz).
Ishora: useOptimistic(reviews, reducer) + <form action>; xato — try/catch va state; «Chat: "yuborilmoqda"» bo'limi.
function postReview(text) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const clean = text.trim();
if (clean.length < 3) reject(new Error("sharh juda qisqa"));
else resolve(clean);
}, 200);
});
}Yechim
import { startTransition, useOptimistic, useState } from "react";
function postReview(text) {
return new Promise((resolve, reject) => {
setTimeout(() => {
const clean = text.trim();
if (clean.length < 3) reject(new Error("sharh juda qisqa"));
else resolve(clean);
}, 200);
});
}
function addPending(current, text) {
return [...current, { text, pending: true }];
}
export default function App() {
const [reviews, setReviews] = useState([]);
const [shown, addOptimistic] = useOptimistic(reviews, addPending);
const [error, setError] = useState(null);
async function sendAction(formData) {
const text = formData.get("review");
addOptimistic(text);
try {
const saved = await postReview(text);
startTransition(() => {
setReviews((r) => [...r, { text: saved, pending: false }]);
setError(null);
});
} catch (err) {
startTransition(() => setError(err.message));
}
}
return (
<form action={sendAction}>
<ul>
{shown.map((r, i) => (
<li key={i}>
{r.text}
{r.pending && " (yuborilmoqda…)"}
</li>
))}
</ul>
<label>
Sharh <input name="review" />
</label>
<button>Yuborish</button>
{error && <p role="alert">Saqlanmadi: {error}</p>}
</form>
);
}Ekranda:
Zo'r osh!
Sharh Yuborish
Saqlanmadi: sharh juda qisqaBirinchi sharh tasdiqlandi va asosiy ro'yxatga yozildi. Ikkinchisi ("ok") 60 ms da ro'yxatda "yuborilmoqda" bilan ko'rindi (ekran), 200 ms da rad etildi — Action tugadi va u o'zi yo'qoldi. Biz faqat xato matnini qo'ydik. key={i} — bu mashq uchun yetarli; haqiqiy chatda server bergan id ishlatiladi (Ro'yxatlar va key).
4-mashq: Vazifalar qadami
refactor/actions-optimistik branch'ida App'ni «Vazifalar qadami» bo'limi bo'yicha o'zgartiring: useOptimistic(asosiy, optimistikOzgartir), yozuv, fondaYoz, songgiAsosiy, fokusniRejala, e'lon — useOptimistic, saqla(asosiy). Reducer'ga serverdanKeldi, paket.ts ga serverVazifasiniTekshir qo'shing. Hammasini tekshirib, commit qiling va PR oching.
Yechim
Tekshiruv ro'yxati:
npm test— node 163, Vitest 33.lint,format:check,tip— toza.- Qo'lda (dev server, DevTools → Network → «Offline» emas, balki serverni to'xtatib): katakni bosing — darhol belgilanadi, «Saqlanmadi: …» e'loni bilan qaytadi. Sahifani yangilang — rad etilgan o'zgarish yo'q (
localStorage— faqat asosiy). - Ikki katakni tez bosing — birinchisi rad etilsa ham, ikkinchisi yo'qolmaydi.
flushSyncfaqat qolgan joylarda (fokus darhol kerak bo'lgan, optimistik bo'lmagan yangilanishlarda).- Commit —
refactor: yozuvlar — action va useOptimistic; forma — useActionState. PR'ni birlashtiring.
10. Real ishda
- Ijtimoiy tarmoqlar va messenjerlar — like, saqlash, xabar yuborish deyarli doim optimistik. Telegram'dagi "soat" belgisi — bugungi "(yuborilmoqda…)".
- TanStack Query —
onMutateda keshni optimistik yangilash vaonErrorda qaytarish (TanStack Query III). U yerda qaytarish qo'lda — bugungi poyga muammosini yodda tuting. - Next.js Server Actions —
useOptimisticserver funksiyasi bilan juft ishlatiladi (Server Actions II). - Intervyu savollari: "Optimistik UI nima va qachon ishlatmaslik kerak?", "
useOptimisticrad etilganda qanday qaytaradi?", "nega optimistik amal idempotent bo'lishi kerak?".
Xulosa
useOptimistic(value, reducer?)→[ekrandagi, setOptimistic];setOptimistic— faqat Action yoki transition ichida.- Ekran = asosiy state + kutilayotgan o'zgarishlar. Action tugasa — uning o'zgarishi qatlamdan chiqadi.
- Rad etilganda qaytarish kodi kerak emas; asosiy state'ga faqat tasdiq yoziladi (updater bilan, eng so'nggisi ustiga).
- Qo'lda "oldingi qiymatni qaytarish" ikki yozuv poygasida boshqasini o'chiradi (v7:
-- → x- → xx → -- → -x, v8:-- → x- → xx → -x). - Optimistik amal — idempotent ("shu qiymat bo'lsin"); to'lov kabi joylarda optimistik emas.
vazifalar: ekran —useOptimistic,localStorageva tablar — faqat asosiy; uch dars — bitta commit, 159/159.
Keyingi dars: Error boundary — render va Action xatosi butun ilovani o'chirmasligi uchun chegara qo'yish va tiklash.
Manbalar
- React:
useOptimistic— react.dev/reference/react/useOptimistic (2026-10 holati, React 19.3) - React:
useTransition— Actions — react.dev/reference/react/useTransition - React blog: "React v19" (2024-12-05) —
useOptimistic— react.dev/blog/2024/12/05/react-19
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!