IlmHamroh
JavaScript Full-stack/18-qism. React chuqur7/42-dars24 daqiqa
Mundarija (33)

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 ichida setOptimistic(...) chaqirsangiz, ekranda darhol yangi qiymat chiqadi. Action tugaganda React optimistik qatlamni o'zi olib tashlaydi va ekranda yana value — haqiqiy state qoladi. Server tasdiqlagan bo'lsa, siz value ni 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]). setOptimistic faqat 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 useOptimistic ni ikki yozuv poygasida solishtirasiz.
  • vazifalar dagi 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

jsx
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:

text
render: 10 10
render: 10 11
render: 11 11

Ekranda:

text
Yoqdi
❤ 11

ekran("❤ 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:

jsx
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:

text
Yoqdi
❤ 10
Saqlanmadi: 500: server xatosi

Bosish — 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'q

Diagrammadagi 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/catch ni 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?

jsx
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:

text
An optimistic state update occurred outside a transition or action. To fix, move the update to an action, or wrap with startTransition.

Ekranda:

text
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:

jsx
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…":

jsx
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:

text
Salom, oshxona!
3-stolga osh
Xabar Yuborish

Bosilgandan 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:

text
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":

jsx
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:

text
Yuborish
a b x

ekran("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:

jsx
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):

text
[ ] Osh[ ] Manti

Ikki 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:

jsx
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:

text
[ ] Osh[x] Manti

Oraliq 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):

text
✅ 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: toggle ichida change nega startTransition dan 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:

tsx
  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):

tsx
// 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):

tsx
  // 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:

tsx
  // 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

tsx
  // 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:

tsx
    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:

tsx
    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:

tsx
  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:

tsx
  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:

bash
git add -A
git commit

Commit (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), Vitest Tests 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.

jsx
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):

text
Obuna bo'lish
Obunasiz
Yechim
jsx
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):

text
Obuna bo'lish
Obuna bo'ldingiz

Ekran 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:

  1. Yuborilgan sharh ro'yxatda darhol chiqsin, yonida " (yuborilmoqda…)".
  2. postReview (o'zgartirmang) bo'sh yoki 3 harfdan qisqa sharhni rad etadi. Rad etilsa — sharh ro'yxatdan o'zi yo'qolsin va role="alert" da "Saqlanmadi: …" chiqsin.
  3. Tasdiqlangan sharh asosiy ro'yxatga yozilsin (" (yuborilmoqda…)" siz).

Ishora: useOptimistic(reviews, reducer) + <form action>; xato — try/catch va state; «Chat: "yuborilmoqda"» bo'limi.

jsx
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
jsx
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:

text
Zo'r osh!
Sharh Yuborish
Saqlanmadi: sharh juda qisqa

Birinchi 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.
  • flushSync faqat 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 — onMutate da keshni optimistik yangilash va onError da qaytarish (TanStack Query III). U yerda qaytarish qo'lda — bugungi poyga muammosini yodda tuting.
  • Next.js Server Actions — useOptimistic server funksiyasi bilan juft ishlatiladi (Server Actions II).
  • Intervyu savollari: "Optimistik UI nima va qachon ishlatmaslik kerak?", "useOptimistic rad 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, localStorage va 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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
useOptimistic: darhol javob beradigan UI — optimistik state va avtomatik qaytarish — IlmHamroh