IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar15/36-dars26 daqiqa
Mundarija (32)

TanStack Query useMutation: serverga yozish va keshni yangilash

Qisqacha: O'qish — useQuery, yozish — useMutation. mutate(qiymat) serverga so'rov yuboradi. Server "bajarildi" degach, keshni ikki yo'l bilan yangilaysiz: invalidateQueries (eskirdi — qayta so'ra) yoki setQueryData (javobni keshga o'zing yoz). Optimistik yangilashda ekran serverni kutmaydi: avval o'zgartiriladi, xato bo'lsa — qaytariladi.

Bu darsda

  • useMutation bilan serverga yozasiz: mutate, mutateAsync, isPending, isError, variables.
  • onSuccess, onError, onSettled qachon chaqirilishini tartib bilan ko'rasiz.
  • Yozuvdan keyin keshni invalidateQueries yoki setQueryData bilan yangilaysiz va qaysi biri qachon to'g'ri ekanini bilasiz.
  • Optimistik yangilashning ikki usulini yozasiz: variables orqali va onMutate + qaytarish.
  • Bir vaqtdagi ikki yozuvda snapshot qaytarish nega xato berishini ko'rasiz. vazifalar dagi holat 14 ni commit qilasiz.

Oldin bilishingiz kerak: TanStack Query II: kesh hayot sikli va kalitlar, TanStack Query I: useQuery asoslari, useOptimistic, fetch bilan ma'lumot yuborish.

1. Nega bu kerak?

Ikki darsda faqat o'qidik. Lekin «Bahor» saytida mehmon yozadi ham: taomni savatga qo'shadi, stol band qiladi, sharh qoldiradi. Jasur aka esa admin panelda yangi taom qo'shadi.

Yozish o'qishdan murakkabroq. Taom qo'shildi — endi keshdagi menyu eskirdi. Uni kim yangilaydi? Qachon? Server xato bersa-chi? Tugmani ikki marta bossa-chi? Bu savollarni har yozuv uchun qo'lda yechish — 17–18-qismdagi vazifalar "xo'jaligi": kutilmoqda hisobi, pult, eng so'nggi ro'yxat uchun ref.

O'xshatish: restoranda buyurtma qog'ozi. Ofitsiant buyurtmani oshxonaga beradi (yozuv). Oshxona "qabul qildik" desa — zal doskasidagi "band stollar" ro'yxati yangilanishi kerak. Ofitsiant doskani o'chirib, oshxonadan to'liq ro'yxatni qayta so'rashi mumkin (invalidatsiya). Yoki bitta qatorni o'zi yozib qo'yishi mumkin (setQueryData). Shoshilgan ofitsiant esa oshxona javobini kutmay doskaga yozadi — oshxona rad etsa, o'chiradi (optimistik yangilash).

2. useMutation asoslari

2.1 Birinchi yozuv

Mutatsiya (mutation) — serverdagi ma'lumotni o'zgartiradigan so'rov: POST, PUT, PATCH, DELETE. JavaScript'dagi "mutatsiya" (obyektni joyida o'zgartirish) bilan ma'nosi yaqin: bu yerda serverdagi holat o'zgaradi.

useQuery dan uch farqi bor:

  1. O'zi ishga tushmaydi. useQuery komponent chiqqanda so'raydi. useMutation esa siz mutate(...) ni chaqirguncha jim.
  2. Kalit shart emas. Yozuv keshga tushmaydi, u keshni o'zgartiradi.
  3. Qayta urinmaydi (sukut retry: 0). Ikki marta "taom qo'sh" — ikkita taom bo'lishi mumkin.

Natija oynasida server — modul darajasidagi massiv. addDish unga 200 ms dan keyin yozadi:

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

// Soxta server
const serverMenu = ["Osh", "Manti"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  console.log("GET");
  await wait(100);
  return [...serverMenu];
}

async function addDish(name) {
  console.log("POST", name);
  await wait(200);
  serverMenu.push(name);
  return name;
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: addDish,
    onSuccess: () =>
      client.invalidateQueries({ queryKey: ["menu"] }),
  });
  return (
    <>
      <p>{data?.join(", ")}</p>
      <button
        disabled={mutation.isPending}
        onClick={() => mutation.mutate("Lag'mon")}
      >
        {mutation.isPending ? "Saqlanmoqda…" : "Qo'shish"}
      </button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Konsolda:

text
GET
POST Lag'mon
GET

Ekranda:

text
Osh, Manti, Lag'mon
Qo'shish

Ketma-ketlik: mutate("Lag'mon") → addDish("Lag'mon") → server "bajarildi" → onSuccess → invalidateQueries(["menu"]) → menyu eskirdi va ekranda bo'lgani uchun darhol qayta so'raldi (ikkinchi GET). Yangi taom ekranga serverdan keldi.

mutate ga bergan qiymatingiz mutationFn ning argumenti bo'ladi. Bir nechta qiymat kerak bo'lsa — obyekt: mutate({ name, price }).

Tugma disabled={mutation.isPending} — ikki marta bosishdan himoya. Mehmon sekin internetda "bosildimi?" deb qayta bosadi. Bu qatorsiz ikkita "Lag'mon" bo'lardi.

2.2 Mutatsiya holati

useMutation natijasi useQuery nikiga o'xshaydi:

Maydon Ma'nosi
isIdle hali mutate chaqirilmagan
isPending so'rov yo'lda
isError / error server rad etdi, sababi
isSuccess / data bajarildi, server javobi
variables mutate ga berilgan qiymat
reset() holatni boshiga (idle) qaytaradi

status bitta: "idle" | "pending" | "error" | "success". fetchStatus yo'q — yozuv fonda o'zi takrorlanmaydi.

reset() xato xabarini yopish uchun kerak: foydalanuvchi xatoni o'qidi, "Yopish" ni bosdi — holat idle, xabar yo'qoladi.

2.3 mutate va mutateAsync

Ikki chaqirish usuli bor:

  • mutate(qiymat) — hech narsa qaytarmaydi, xatoni ham tashlamaydi. Natija — maydonlarda va callback'larda. Tugmalar uchun shu yetarli.
  • mutateAsync(qiymat) — Promise qaytaradi. await qilsa bo'ladi, xato bo'lsa — tashlaydi. Yozuvdan keyin ketma-ket ish kerak bo'lsa (forma action'i, "saqla va keyingi sahifaga o't") — shu.

mutateAsync ni try/catch siz ishlatmang. Xato ushlanmasa, brauzer konsolida "Uncaught (in promise)" chiqadi. vazifalar dagi useYozuv aynan mutateAsync + try/catch ni ishlatadi («Vazifalar qadami» bo'limida).

3. Callback'lar: onSuccess, onError, onSettled

3.1 Tartib

Mutatsiyada to'rtta "ilgak" (callback) bor:

Callback Qachon Odatiy ish
onMutate mutationFn dan oldin optimistik o'zgarish
onSuccess server bajardi keshga yozish, e'lon
onError server rad etdi qaytarish, xato e'loni
onSettled har holatda, oxirida invalidatsiya

onSettled — try/catch dagi finally kabi: muvaffaqiyat ham, xato ham bo'lsa — chaqiriladi. Butun yo'l rasmda — komponent, mutatsiya, server va kesh:

sequenceDiagram
  participant K as Komponent
  participant M as useMutation
  participant S as Server
  participant Q as Kesh
  K->>M: mutate("Lag'mon")
  M->>Q: onMutate (optimistik)
  M->>S: mutationFn (POST)
  S-->>M: javob yoki xato
  M->>Q: onSuccess / onError
  M->>Q: onSettled (invalidatsiya)
  Q->>S: GET (ekranda bo'lsa)
  Q-->>K: yangi data

Tartibni konsolda ko'ramiz. Ikkinchi bosish ataylab xato beradi:

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

function orderDish(name) {
  console.log("mutationFn:", name);
  return new Promise((resolve, reject) =>
    setTimeout(() => {
      if (name === "Shashlik") reject(new Error("Shashlik tugadi"));
      else resolve(`${name} qabul qilindi`);
    }, 100),
  );
}

function Order() {
  const mutation = useMutation({
    mutationFn: orderDish,
    onMutate: (name) => console.log("onMutate:", name),
    onSuccess: (data) => console.log("onSuccess:", data),
    onError: (error) => console.log("onError:", error.message),
    onSettled: () => console.log("onSettled"),
  });
  return (
    <>
      <button onClick={() => mutation.mutate("Osh")}>Osh</button>{" "}
      <button onClick={() => mutation.mutate("Shashlik")}>
        Shashlik
      </button>
      <p>
        {mutation.status}
        {mutation.isError && `: ${mutation.error.message}`}
      </p>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Order />
    </QueryClientProvider>
  );
}

Konsolda:

text
onMutate: Osh
mutationFn: Osh
onSuccess: Osh qabul qilindi
onSettled
onMutate: Shashlik
mutationFn: Shashlik
onError: Shashlik tugadi
onSettled

Ekranda:

text
Osh Shashlik
error: Shashlik tugadi

onSuccess ning birinchi argumenti — server javobi (mutationFn qaytargan qiymat). onError niki — xato. Ikkalasi ikkinchi argumentda variables ni ham oladi: onError: (error, name) => ….

3.2 Callback qayerda: hook'dami yoki mutate dami

Callback'larni ikki joyda berish mumkin: useMutation({ onSuccess }) va mutate(qiymat, { onSuccess }). Farqi muhim:

  • Hook'dagi — har doim ishlaydi, komponent yopilsa ham. Kesh bilan ishlash shu yerda: invalidatsiya, setQueryData.
  • mutate dagi — faqat komponent hali ekranda bo'lsa. UI ishlari shu yerda: formani tozalash, sahifaga o'tish, modalni yopish.

Sabab: foydalanuvchi "Saqlash" ni bosib, javobni kutmay boshqa sahifaga o'tsa, kesh baribir yangilanishi kerak. Formani tozalash esa — yopilgan formada ma'nosiz.

Tekshirib ko'ring: onSuccess da invalidateQueries, onSettled da ham invalidateQueries yozildi. Muvaffaqiyatli yozuvdan keyin menyu necha marta qayta so'raladi?

Javob

Bu onSuccess Promise'ni qaytarish-qaytarmasligiga bog'liq. Qavssiz o'q funksiya (onSuccess: () => client.invalidateQueries(...)) Promise'ni qaytaradi. Mutatsiya uni kutadi: birinchi GET to'liq tugaydi, keyin onSettled ikkinchi GET'ni yuboradi — ikki to'liq so'rov. onSuccess qaytarmasa ({ … } ichida, return siz), ikkala invalidatsiya bir zumda keladi. Ikkinchisi birinchi GET'ni bekor qiladi (sukut cancelRefetch: true) — ikki so'rov boshlanadi, javob bittasidan olinadi. Ikkala holatda ham bu ortiqcha kod: invalidatsiyani bitta joyda yozing — odatda onSettled da, chunki xatodan keyin ham kesh server bilan solishtirilishi kerak.

4. Keshni yangilashning ikki yo'li

4.1 invalidateQueries — "qayta so'ra"

Birinchi misolda shu yo'lni ko'rdik. Afzalliklari:

  • oddiy — kesh qanday tuzilganini bilish shart emas;
  • to'g'ri — ekrandagi narsa doim serverdan, server qo'shgan maydonlar (id, sana, tartib) ham.

Kamchiligi — qo'shimcha so'rov va ekranning bir zum eski turishi (POST tugagach, GET kelguncha).

Bitta muhim nuqta: invalidateQueries Promise qaytaradi — qayta so'rov tugaganda bajariladi. Callback'dan uni qaytarsangiz (onSuccess: () => client.invalidateQueries(...) — qavssiz o'q funksiya qaytaradi), mutatsiya isPending holatida GET tugaguncha qoladi. Tugma "Saqlanmoqda…" yangi ro'yxat kelguncha turadi — foydalanuvchi oraliq eski holatni ko'rmaydi. Birinchi misolda aynan shunday.

4.2 setQueryData — "o'zim yozaman"

Ko'p API yozuvdan keyin yangi obyektni qaytaradi: POST /dishes → { id: 3, name: "Lag'mon" }. Uni keshga o'zimiz qo'shsak, GET kerak emas. setQueryData(kalit, yangilovchi) — keshdagi qiymatni almashtiradi. Yangilovchi — funksiya: eski qiymatni oladi, yangisini qaytaradi (State II: updater funksiya kabi):

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = [{ id: 1, name: "Osh" }];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  console.log("GET");
  await wait(100);
  return [...serverMenu];
}

async function addDish(name) {
  console.log("POST", name);
  await wait(200);
  const dish = { id: serverMenu.length + 1, name };
  serverMenu.push(dish);
  return dish;
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: addDish,
    onSuccess: (dish) =>
      client.setQueryData(["menu"], (old) => [...old, dish]),
  });
  return (
    <>
      <ul>
        {data?.map((d) => (
          <li key={d.id}>
            {d.id}. {d.name}
          </li>
        ))}
      </ul>
      <button onClick={() => mutation.mutate("Lag'mon")}>
        Qo'shish
      </button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Konsolda:

text
GET
POST Lag'mon

Ekranda:

text
1. Osh
2. Lag'mon
Qo'shish

Ikkinchi GET yo'q. Yangi taom serverning javobidan (id: 2 bilan) to'g'ridan-to'g'ri keshga tushdi.

Diqqat: setQueryData da keshni o'zgartirmang, yangisini qaytaring. old.push(dish); return old; — xato: massiv o'sha, Query va React o'zgarishni sezmaydi, ekran yangilanmaydi. State III: immutable yangilash dagi qoida bu yerda ham kuchda: [...old, dish].

4.3 Qaysi birini tanlash

Vaziyat Yo'l
server to'liq yangi obyektni qaytaradi, kesh oddiy setQueryData
bitta yozuv ko'p keshga ta'sir qiladi (ro'yxat, son, statistika) invalidateQueries
server tartiblaydi, hisoblaydi (reyting, jami) invalidateQueries
ishonch yo'q invalidateQueries

Ko'pincha ikkalasi birga: onSuccess da setQueryData (darhol to'g'ri ekran), onSettled da invalidateQueries (keyin server bilan solishtirish). vazifalar aynan shunday qiladi.

5. Optimistik yangilash

5.1 variables orqali — eng oddiy usul

useOptimistic darsida bu g'oyani ko'rgan edik: ekran serverni kutmaydi. Query'da eng oddiy usul — keshga tegmaslik. Yo'ldagi mutatsiyaning variables i bor — uni ro'yxat oxirida "kutilmoqda" belgisi bilan chizamiz:

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = ["Osh", "Manti"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  await wait(100);
  return [...serverMenu];
}

async function addDish(name) {
  await wait(300);
  serverMenu.push(name);
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: addDish,
    onSettled: () =>
      client.invalidateQueries({ queryKey: ["menu"] }),
  });
  return (
    <>
      <ul>
        {data?.map((name) => (
          <li key={name}>{name}</li>
        ))}
        {mutation.isPending && (
          <li style={{ opacity: 0.5 }}>
            {mutation.variables} (saqlanmoqda…)
          </li>
        )}
      </ul>
      <button onClick={() => mutation.mutate("Lag'mon")}>
        Qo'shish
      </button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Ekranda:

text
Osh
Manti
Lag'mon
Qo'shish

Bosish bilan "Lag'mon (saqlanmoqda…)" xira holda paydo bo'ldi. Server tasdiqlab, yangi ro'yxat kelgach — oddiy qator. onSettled invalidatsiyani qaytaradi, shuning uchun isPending GET tugaguncha rost. Xira qator yangi ro'yxat kelishi bilan bir zumda almashadi — "miltillash" yo'q.

Xato bo'lsa — isPending yolg'on bo'ladi va xira qator o'zi yo'qoladi. Qaytarish uchun hech narsa yozish shart emas: keshga tegmagan edik. mutation.isError da qatorni qizil rangda "Qayta urinish" bilan qoldirish ham mumkin.

Kamchiligi: xira qator faqat shu komponentda. Menyu ikki joyda chizilsa (ro'yxat va "nechta taom" hisoblagichi), hisoblagich eski sonni ko'rsatadi. Boshqa komponentda kerak bo'lsa — useMutationState (hamma mutatsiyalar holati, mutationKey bo'yicha) yoki keyingi usul.

5.2 onMutate orqali — keshning o'zini o'zgartirish

Ikkinchi usul keshni o'zgartiradi. Kesh hamma komponentga bitta — demak o'zgarish hamma joyda darhol ko'rinadi. To'rt qadam:

  1. cancelQueries — yo'ldagi GET'larni bekor qilish. Aks holda eski javob kelib, optimistik o'zgarishni bosib ketadi.
  2. Snapshot — getQueryData bilan hozirgi qiymatni eslab qolish.
  3. setQueryData — optimistik o'zgarish. Snapshot onMutate dan qaytariladi.
  4. onError — snapshot'ni qaytarish. onSettled — server bilan solishtirish.

Server bu safar rad etadi:

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = ["Osh", "Manti"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  await wait(100);
  return [...serverMenu];
}

async function addDish(name) {
  await wait(300);
  throw new Error(`${name} qo'shilmadi: ruxsat yo'q`);
}

function Count() {
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  return <p>Jami: {data?.length} ta</p>;
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: addDish,
    onMutate: async (name) => {
      await client.cancelQueries({ queryKey: ["menu"] });
      const previous = client.getQueryData(["menu"]);
      client.setQueryData(["menu"], (old) => [...old, name]);
      console.log("optimistik:", name);
      return { previous };
    },
    onError: (error, name, result) => {
      console.log("qaytarildi:", error.message);
      client.setQueryData(["menu"], result.previous);
    },
    onSettled: () =>
      client.invalidateQueries({ queryKey: ["menu"] }),
  });
  return (
    <>
      <p>{data?.join(", ")}</p>
      <button onClick={() => mutation.mutate("Shashlik")}>
        Shashlik qo'shish
      </button>
      {mutation.isError && <p>{mutation.error.message}</p>}
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
      <Count />
    </QueryClientProvider>
  );
}

Konsolda:

text
optimistik: Shashlik
qaytarildi: Shashlik qo'shilmadi: ruxsat yo'q

Ekranda:

text
Osh, Manti
Shashlik qo'shish
Shashlik qo'shilmadi: ruxsat yo'q
Jami: 2 ta

Bosish bilan "Osh, Manti, Shashlik" va "Jami: 3 ta" chiqdi — ikkala komponentda bir vaqtda. 300 ms dan keyin server rad etdi, onError snapshot'ni qaytardi: yana 2 ta. Ekranda — sababi.

onError ning uchinchi argumenti (result) — onMutate qaytargan obyekt. Snapshot'ni callback'lar orasida shu yo'l bilan tashiymiz.

Ikki usulni yonma-yon qo'yamiz:

variables orqali onMutate + qaytarish
kod kam ko'p (4 qadam)
kesh tegilmaydi o'zgartiriladi
boshqa komponentlar ko'rmaydi darhol ko'radi
xatoda o'zi yo'qoladi qo'lda qaytarish

5.3 Tuzoq: bir vaqtdagi ikki yozuv

onMutate usulida yashirin xato bor. Mehmon tez-tez ikki taom qo'shdi: Lag'mon va Manti. Server Lag'monni rad etadi (300 ms), Mantini qabul qiladi (900 ms). Nima bo'ladi?

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = ["Osh", "Somsa"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  await wait(100);
  return [...serverMenu];
}

async function addDish(name) {
  if (name === "Lag'mon") {
    await wait(300);
    throw new Error("Lag'mon tugadi");
  }
  await wait(900);
  serverMenu.push(name);
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: addDish,
    onMutate: async (name) => {
      await client.cancelQueries({ queryKey: ["menu"] });
      const previous = client.getQueryData(["menu"]);
      client.setQueryData(["menu"], (old) => [...old, name]);
      return { previous };
    },
    onError: (error, name, result) => {
      console.log("qaytarildi:", result.previous.join(", "));
      client.setQueryData(["menu"], result.previous);
    },
    onSettled: () =>
      client.invalidateQueries({ queryKey: ["menu"] }),
  });
  return (
    <>
      <p>
        {data?.join(", ")} — Jami: {data?.length} ta
      </p>
      <button onClick={() => mutation.mutate("Lag'mon")}>
        Lag'mon
      </button>{" "}
      <button onClick={() => mutation.mutate("Manti")}>Manti</button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Konsolda:

text
qaytarildi: Osh, Somsa

Ekranda:

text
Osh, Somsa, Manti — Jami: 3 ta
Lag'mon Manti

Oxiri to'g'ri: 3 ta. Lekin yo'lda nima bo'lganini harakat ning oraliq nuqtalari ko'rsatadi:

Vaqt Voqea Ekranda
0 Lag'mon bosildi: snapshot [Osh, Somsa] 3 ta
~60 ms Manti bosildi: snapshot [Osh, Somsa, Lag'mon] 4 ta
300 ms Lag'mon rad etildi → snapshot qaytdi 2 ta — Manti ham yo'qoldi!
~960 ms Manti tasdiqlandi → GET 3 ta

Lag'monning snapshot'i Mantidan oldin olingan edi. Uni qaytarish Mantining optimistik qatorini ham o'chirdi. Manti hali serverda kutilyapti, lekin mehmon uni ekranda yo'qolganini ko'rdi va qaytadan bosishi mumkin — ikkita Manti.

Bu useOptimistic darsidagi xatoning o'zi. Davolash yo'llari:

  • Snapshot qaytarmaslik. Xatoda invalidateQueries — server haqiqatni aytadi. Oraliqda bir zum noaniq, lekin noto'g'ri emas.
  • Optimistik qatlamni keshdan ajratish. React'ning useOptimistic i shunday ishlaydi: ekran = server tasdiqlagan ro'yxat + hali kutilayotgan amallar. Bitta amal rad etilsa, faqat u qatlamdan chiqadi, boshqalari qoladi. variables usuli ham shu g'oyaga yaqin.
  • Oxirgi yozuvda invalidatsiya. Har onSettled da so'rash o'rniga — faqat yo'lda boshqa yozuv qolmaganda. Aks holda birinchi yozuvdan keyingi GET ikkinchisining optimistik o'zgarishini bosib ketadi.

vazifalar oxirgi ikkitasini tanladi.

Tekshirib ko'ring: onMutate da cancelQueries ni olib tashlasak, qaysi holatda optimistik qator "sakrab" yo'qolib, keyin qaytadi?

Javob

Mutatsiya boshlangan paytda menyu GET'i yo'lda bo'lsa (masalan, oynaga qaytilganda fonda yangilanayotgan bo'lsa). Optimistik qator keshga qo'shiladi, keyin eski GET javobi keladi va keshni qatorsiz ro'yxat bilan almashtiradi. Server Lag'monni tasdiqlab, onSettled dagi GET kelgach, qator yana chiqadi. cancelQueries yo'ldagi GET'ni bekor qilib, shu sakrashning oldini oladi.

6. Vazifalar qadami: yozish — useMutation, commit (holat 14)

6.1 Qaror: optimistik qatlam — React'da, tasdiq — Query'da

vazifalar da 18-qismdan beri ekrandagi darhol o'zgarish useOptimistic bilan. Kanon uni o'zgartirmadi. Query'ga faqat server qismi ko'chdi: yozuvni yuborish, tasdiqni keshga qo'yish, oxirida server bilan solishtirish.

Sabab — «Bir vaqtdagi ikki yozuv» bo'limidagi tuzoq. onMutate + snapshot bir vaqtdagi ikki yozuvda 18-qismdagi o'sha xatoni qaytarardi. useOptimistic esa qatlamni har safar "asosiy ro'yxat + kutilayotgan amallar" dan qayta hisoblaydi.

6.2 useYozuv va tasdiqniQoy (server-sinxron.ts)

Kanon (izohlar telefonga sig'ishi uchun bo'lingan):

ts
/**
 * Server tasdiqlagan amal — keshga (va boshqa tablarga).
 * Updater ENG SO'NGGI keshni oladi (bir vaqtdagi yozuvlar
 * bir-birini yo'qotmaydi)
 */
export function tasdiqniQoy(
  mijoz: QueryClient,
  tasdiq: VazifaAmali,
): void {
  const yangi = mijoz.setQueryData<Vazifalar>(royxatKaliti, (eski) =>
    eski === undefined
      ? eski
      : vazifalarReducer(boshlangichHolat(eski), tasdiq).vazifalar,
  );
  if (yangi !== undefined) {
    tablargaYubor(yangi);
  }
}

setQueryData ning yangilovchi funksiyasi — eng so'nggi keshni oladi. 17-qismdagi songgiAsosiy ref'i ("await'dan keyin eng so'nggi ro'yxat") shu sabab bilan kerak edi — endi Query beradi. Tasdiqni qo'llash — 17-qismdagi sof vazifalarReducer. setQueryData yangi qiymatni qaytaradi — u boshqa tablarga yuboriladi (Oyna va tablar orasida aloqa).

ts
const YOZUV_KALITI = ["yozuv"] as const;

/**
 * Yozuv (useMutation, 19/#15): ekrandagi optimistik qatlam —
 * App'ning useOptimistic'i (18/#07); bu yerda — server qismi.
 * Boshlanishida yo'ldagi GET bekor qilinadi (eski ro'yxat
 * tasdiqni bosib ketmasin); tasdiq — keshga; OXIRGI yozuv
 * tugagach — serverdan qayta (invalidateQueries)
 */
export function useYozuv() {
  const mijoz = useQueryClient();
  const mutatsiya = useMutation({
    mutationKey: YOZUV_KALITI,
    mutationFn: async (serverda: () => Promise<VazifaAmali>) => {
      await mijoz.cancelQueries({ queryKey: royxatKaliti });
      return serverda();
    },
    onSuccess: (tasdiq) => tasdiqniQoy(mijoz, tasdiq),
    onSettled: () => {
      // isMutating shu yozuvni ham sanaydi (u hali tugamagan)
      if (mijoz.isMutating({ mutationKey: YOZUV_KALITI }) === 1) {
        void mijoz.invalidateQueries({ queryKey: royxatKaliti });
      }
    },
  });
  return async (
    serverda: () => Promise<VazifaAmali>,
  ): Promise<YozuvNatijasi> => {
    try {
      await mutatsiya.mutateAsync(serverda);
      return { ok: true };
    } catch (xato) {
      return { ok: false, xabar: `Saqlanmadi: ${xatoMatni(xato)}` };
    }
  };
}

Bugungi darsdan hammasi bir joyda:

  • mutationFn argumenti — funksiya (serverda). vazifalar da yozuv turlari ko'p: qo'shish, belgilash, tahrir, o'chirish, import. Har biri api ning o'z metodini chaqiradi va server tasdiqlagan amalni qaytaradi. Bitta mutatsiya hammasiga xizmat qiladi.
  • cancelQueries — boshida, «onMutate orqali» bo'limidagi birinchi qadam.
  • onSuccess — setQueryData (tasdiqni qo'yish), onSettled — invalidateQueries.
  • mutationKey va isMutating — "yo'lda nechta yozuv bor". === 1 — faqat shu yozuv qoldi, ya'ni u oxirgisi. 17-qismdagi kutilmoqda hisobi shu.
  • mutateAsync + try/catch — App'ning async action'i natijani kutadi va xatoni e'longa aylantiradi.

6.3 App: useOptimistic endi tashqi store ustida

App.tsx da yozuv bitta qatorga qisqardi (kanon):

diff
     optimistik(amal);
-    const natija = await yozuvniYubor(
-      serverda,
-      s,
-      () => songgiAsosiy.current,
-    );
+    const natija = await yoz(serverda); // 19/#15: useMutation
     if (!natija.ok) {
       elonQil(natija.xabar);
     }

Bitta nozik o'zgarish bor. Asosiy ro'yxat endi Query keshida — React uchun bu tashqi store. Tasdiq keshga action tugashidan oldin tushadi (onSuccess da). React esa hali kutilayotgan optimistik amalni yangi ro'yxat ustiga qayta qo'llaydi. Ya'ni "qoshildi" amali ikki marta qo'llanishi mumkin: bir marta server tasdiqlagan ro'yxatda, yana bir marta optimistik qatlamda. Natija — ekranda bir zum ikkita bir xil vazifa.

Davosi — amalni idempotent qilish: ikki marta qo'llansa ham natija o'sha (kanon):

diff
+// 19/#13: asosiy ro'yxat endi Query keshida (tashqi store) —
+// tasdiq keshga action tugashidan OLDIN tushadi va React
+// kutilayotgan amalni YANGI ro'yxat ustiga qayta qo'llaydi.
+// Shuning uchun amal ikki marta qo'llansa ham natija o'sha
+// bo'lishi kerak: "qoshildi" — shu matnli vazifa allaqachon
+// bo'lsa (server tasdiqlagan), yana qo'shilmaydi (bir xil
+// matnli ikki vazifa baribir bo'lmaydi — xatoniTop)
 function optimistikOzgartir(
   vazifalar: readonly VazifaMalumoti[],
   amal: VazifaAmali,
 ): readonly VazifaMalumoti[] {
+  if (
+    amal.tur === "qoshildi" &&
+    vazifalar.some((v) => v.matn === amal.matn)
+  ) {
+    return vazifalar;
+  }
   return vazifalarReducer(boshlangichHolat(vazifalar), amal).vazifalar;
 }

"Belgilash" va "o'chirish" allaqachon idempotent edi (18-qismdagi izoh: "almashtir" ikki marta — teskarisi emas, qiymat bilan). Faqat "qo'shish" ga shart kerak bo'ldi. Bir xil matnli ikki vazifa ilovada baribir taqiqlangan (xatoniTop), shuning uchun shart xavfsiz.

6.4 Vazifa sahifasi: action ham o'sha kesh

Vazifa sahifasining action'i (o'chirish, belgilash) hook ishlata olmaydi. U sorovMijozi ni to'g'ridan oladi va o'sha tasdiqniQoy ni chaqiradi (kanon):

ts
// Server tasdiqlagan o'zgarish — Query keshiga va boshqa
// tablarga (tasdiqniQoy), brauzer nusxasiga (localStorage);
// keyin serverdan qayta
function tasdiqlandi(tasdiq: Parameters<typeof tasdiqniQoy>[1]): void {
  tasdiqniQoy(sorovMijozi, tasdiq);
  const yangi = sorovMijozi.getQueryData<Vazifalar>(royxatKaliti);
  if (yangi !== undefined) {
    saqla(yangi);
  }
  void sorovMijozi.invalidateQueries({ queryKey: royxatKaliti });
}

Vazifa sahifasida o'chirilgan vazifa ro'yxatga qaytganda yo'q — so'rovsiz, chunki kesh bitta.

6.5 Testlar va commit

Vitest testlari endi Query ichida chiziladi. Har test — o'z QueryClient i, aks holda testlar bir-birining keshini ko'radi (kanon, App.test.tsx):

tsx
function Ilova({ children }: { children: ReactNode }) {
  const [mijoz] = useState(
    () =>
      new QueryClient({
        defaultOptions: {
          queries: { retry: false, staleTime: 30_000 },
        },
      }),
  );
  return (
    <QueryClientProvider client={mijoz}>
      <BrowserRouter>{children}</BrowserRouter>
    </QueryClientProvider>
  );
}

Va har testdan keyin:

ts
afterEach(() => {
  vi.restoreAllMocks();
  // 19/#13: Query "offline" hodisasini global eslab qoladi
  // (onlineManager) — keyingi testlarning so'rovlari to'xtab
  // qolmasin
  onlineManager.setOnline(true);
});

onlineManager — modul darajasidagi bitta obyekt. "Internet uzildi" testi uni false qiladi, keyingi test esa so'rovni paused da kutib qoladi. Bu xato faqat testlar ma'lum tartibda ishlaganda chiqadi — shuning uchun tozalash har testdan keyin.

bash
git add -A
git commit

Commit (kanon): refactor: server holati — TanStack Query (useQuery, useMutation), bitta kesh sahifalar uchun.

Tekshiruvlar (haqiqiy, holat 14):

  • npm test — node:test ℹ tests 174, Vitest Tests 56 passed (56). «Orqaga» tuzatishidan (React Router III) keyin kanonda — 182 (yangi tarix-chuqurligi.test.js dagi 8 ta test).
  • lint, format:check, tip, knip — exit 0. Build: index-….js 417.08 kB │ gzip: 131.46 kB. Query bilan +9.9 KB gzip.
  • Brauzer: 13–17-qism tekshiruvlari 159/159, 18-qismniki 72/72, 19-qismniki 64/64 (kanondagi «Orqaga» va /kirish tuzatishlaridan keyin 67) — oxirgi to'rttasi oldingi darsda ko'rgan "server holati" tekshiruvlari.
  • Diff: 9 fayl, +350 −225. Qo'shilgandan ko'p o'chirilgani yo'q, lekin server-sinxron.ts dagi qo'lda yozilgan "xo'jalik" (sinxronla, sinxronniTox, yozuvniYubor, pult, kutilmoqda) butunlay ketdi.

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
qo'shildi, lekin ro'yxatda yo'q kesh yangilanmagan invalidateQueries yoki setQueryData
setQueryData dan keyin ekran o'zgarmadi eski massiv o'zgartirilgan (push) yangi massiv qaytaring
ikki marta bosildi — ikkita yozuv tugma isPending da faol disabled={mutation.isPending}
konsolda Uncaught (in promise) mutateAsync try/catch siz try/catch yoki mutate
optimistik qator sakrab yo'qoladi cancelQueries yo'q onMutate boshida cancelQueries
bitta xato boshqa yozuvni ham o'chirdi snapshot qaytarish + bir vaqtdagi yozuvlar invalidatsiya yoki useOptimistic
sahifadan chiqqach kesh yangilanmadi kesh ishi mutate(…, { onSuccess }) da kesh ishi — hook'dagi callback'da
"Saqlanmoqda…" GET tugaguncha turadi onSettled invalidatsiyani qaytaradi kutish kerak bo'lmasa — void bilan

Oxirgisi xato emas, tanlov. Ko'pincha kutish yaxshiroq — ekran eski holatni ko'rsatmaydi. Lekin GET sekin bo'lsa, tugmani band qilmasdan void client.invalidateQueries(...) deb yozing.

8. Mashqlar

Mashqlar kurs/mashqlar/19/15-mutatsiya/ da.

1-mashq (oson): callback'lar tartibi

Server xato berdi. Callback'lar qaysi tartibda chaqiriladi? Raqam qo'ying (1–4).

  • onError →
  • onMutate →
  • onSettled →
  • mutationFn →
Yechim

onMutate (1) — so'rovdan oldin. mutationFn (2) — so'rovning o'zi. onError (3) — rad etildi. onSettled (4) — har holatda oxirida. Muvaffaqiyatda 3-o'rinda onSuccess turardi. «Tartib» bo'limidagi konsolga qarang.

2-mashq (o'rta): o'chirish va kesh

Ro'yxatda «O'chirish» tugmasi bor. Serverda o'chadi, ekranda — yo'q. useMutation ga bitta callback qo'shing: muvaffaqiyatdan keyin keshdan shu taomni setQueryData bilan olib tashlasin (GET'siz). Tugma isPending da o'chiq bo'lsin.

jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = ["Osh", "Manti", "Lag'mon"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  console.log("GET");
  await wait(100);
  return [...serverMenu];
}

async function removeDish(name) {
  console.log("DELETE", name);
  await wait(200);
  serverMenu.splice(serverMenu.indexOf(name), 1);
  return name;
}

function Menu() {
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({ mutationFn: removeDish });
  return (
    <>
      <p>{data?.join(", ")}</p>
      <button onClick={() => mutation.mutate("Manti")}>
        Manti ni o'chirish
      </button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Ekranda:

text
Osh, Manti, Lag'mon
Manti ni o'chirish
Yechim
jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

const serverMenu = ["Osh", "Manti", "Lag'mon"];
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchMenu() {
  console.log("GET");
  await wait(100);
  return [...serverMenu];
}

async function removeDish(name) {
  console.log("DELETE", name);
  await wait(200);
  serverMenu.splice(serverMenu.indexOf(name), 1);
  return name;
}

function Menu() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["menu"],
    queryFn: fetchMenu,
  });
  const mutation = useMutation({
    mutationFn: removeDish,
    onSuccess: (removed) =>
      client.setQueryData(["menu"], (old) =>
        old.filter((name) => name !== removed),
      ),
  });
  return (
    <>
      <p>{data?.join(", ")}</p>
      <button
        disabled={mutation.isPending}
        onClick={() => mutation.mutate("Manti")}
      >
        Manti ni o'chirish
      </button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Menu />
    </QueryClientProvider>
  );
}

Konsolda:

text
GET
DELETE Manti

Ekranda:

text
Osh, Lag'mon
Manti ni o'chirish

filter yangi massiv qaytaradi — keshni o'zgartirmaydi, almashtiradi. Ikkinchi GET yo'q. removeDish o'chirilgan nomni qaytargani uchun onSuccess uni birinchi argumentda oldi.

3-mashq (qiyin): optimistik "yoqdi" va qaytarish

Sharhdagi «Yoqdi» tugmasi. Server sekin (300 ms) va har ikkinchi bosishni rad etadi. Yozing:

  1. Kesh: ["likes"] → son (boshida 10).
  2. onMutate — cancelQueries, snapshot, son +1.
  3. onError — snapshot'ni qaytarish va konsolga qaytarildi: <son>.
  4. onSettled — invalidatsiya.
  5. Ekranda: "Yoqdi: N" va tugma.

Server:

jsx
let serverLikes = 10;
let calls = 0;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchLikes() {
  await wait(100);
  return serverLikes;
}

async function like() {
  calls += 1;
  await wait(300);
  if (calls % 2 === 0) throw new Error("Server band");
  serverLikes += 1;
}

Ishora: «onMutate orqali» bo'limidagi to'rt qadam. Tugmani ikki marta, oraliq bilan bosib sinang.

Yechim
jsx
import {
  QueryClient,
  QueryClientProvider,
  useMutation,
  useQuery,
  useQueryClient,
} from "@tanstack/react-query";

const queryClient = new QueryClient();

let serverLikes = 10;
let calls = 0;
const wait = (ms) => new Promise((r) => setTimeout(r, ms));

async function fetchLikes() {
  await wait(100);
  return serverLikes;
}

async function like() {
  calls += 1;
  await wait(300);
  if (calls % 2 === 0) throw new Error("Server band");
  serverLikes += 1;
}

function Likes() {
  const client = useQueryClient();
  const { data } = useQuery({
    queryKey: ["likes"],
    queryFn: fetchLikes,
  });
  const mutation = useMutation({
    mutationFn: like,
    onMutate: async () => {
      await client.cancelQueries({ queryKey: ["likes"] });
      const previous = client.getQueryData(["likes"]);
      client.setQueryData(["likes"], (old) => old + 1);
      return { previous };
    },
    onError: (error, variables, result) => {
      console.log("qaytarildi:", result.previous);
      client.setQueryData(["likes"], result.previous);
    },
    onSettled: () =>
      client.invalidateQueries({ queryKey: ["likes"] }),
  });
  return (
    <>
      <p>Yoqdi: {data}</p>
      <button onClick={() => mutation.mutate()}>Yoqdi</button>
    </>
  );
}

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Likes />
    </QueryClientProvider>
  );
}

Konsolda:

text
qaytarildi: 11

Ekranda:

text
Yoqdi: 11
Yoqdi

Birinchi bosish: darhol 11, server qabul qildi — 11 qoldi. Ikkinchi bosish: darhol 12 (oraliq nuqta buni tekshiradi), server rad etdi — snapshot 11 qaytdi. mutate() argumentsiz — variables undefined, shuning uchun onError ning ikkinchi argumentini ishlatmadik, lekin uchinchisiga yetish uchun yozish shart.

4-mashq: Vazifalar qadami

refactor/tanstack-query branch'ida: tasdiqniQoy, useYozuv, App'dagi yoz(serverda) va idempotent optimistikOzgartir, vazifa-marshruti.ts dagi tasdiqlandi, testlardagi Ilova wrapper va onlineManager.setOnline(true). Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm test — node:test 182 («Orqaga» tuzatishigacha — 174), Vitest 56.
  • npm run dev, Network → "Slow 4G". Ikki vazifani tez-tez qo'shing: ikkalasi darhol ekranda, POST'lar tugagach bitta GET (oxirgi yozuvdan keyin).
  • ?muvaffaqiyatsiz=1 bilan oching (yozuvlar ataylab 500): qo'shilgan vazifa ekranda paydo bo'lib, yo'qoladi, e'lon — "Saqlanmadi: …".
  • Vazifa sahifasida o'chiring → ro'yxatga qayting: vazifa yo'q, yangi GET yo'q (30 s ichida).
  • git log --oneline -1 — refactor: server holati — TanStack Query (useQuery, useMutation), bitta kesh sahifalar uchun.

9. Real ishda

  • CRUD ilovalar (admin panel, CRM, do'kon) — useQuery + useMutation + invalidateQueries ning 90% i. Optimistik yangilash — "yoqdi", savat, belgilash kabi tez va arzon amallarda. To'lov va buyurtmada — yo'q: foydalanuvchi server javobini ko'rishi kerak.
  • Kalitlar bilan ishlash — mutatsiya qaysi keshlarni eskirtirishi kerakligini bilish — eng ko'p xato bo'ladigan joy. Kalit fabrikasi (oldingi dars) bu yerda o'zini oqlaydi: invalidateQueries({ queryKey: dishKeys.all }).
  • Formalar bilan: React Hook Form'ning onSubmit i ichida mutateAsync (RHF + Zod darsida).
  • Next.js da server tomondagi yozuv — Server Actions; klientdagi kesh baribir yangilanishi kerak (Next.js qismida).
  • Intervyu savollari: "invalidateQueries va setQueryData farqi?", "optimistik yangilashni qanday yozasiz va xatoda nima bo'ladi?", "mutate va mutateAsync?", "nega mutatsiya sukut bo'yicha qayta urinmaydi?".

Xulosa

  • useMutation({ mutationFn }) — yozuv. O'zi ishga tushmaydi: mutate(qiymat). Holat: isPending, isError, data, variables, reset().
  • Tartib: onMutate → mutationFn → onSuccess yoki onError → onSettled. Kesh ishi — hook'dagi callback'larda.
  • Kesh: invalidateQueries — oddiy va to'g'ri, bitta GET narxiga; setQueryData — so'rovsiz, yangi qiymat (immutable) bilan.
  • Optimistik: variables — oddiy, faqat shu komponentda; onMutate + snapshot — hamma joyda, lekin bir vaqtdagi yozuvlarda bir-birini o'chiradi.
  • vazifalar: ekrandagi qatlam — useOptimistic, tasdiq — setQueryData, oxirgi yozuvdan keyin — invalidateQueries.

Keyingi dars: TanStack Query IV: sahifalash va cheksiz ro'yxat — placeholderData, useInfiniteQuery, offset va kursor, IntersectionObserver bilan "yana yuklash".

Manbalar

  • TanStack Query v5: Mutations — tanstack.com/query/v5/docs/framework/react/guides/mutations (2026-10, v5.104)
  • TanStack Query v5: Invalidations from Mutations, Updates from Mutation Responses — …/guides/invalidations-from-mutations, …/guides/updates-from-mutation-responses
  • TanStack Query v5: Optimistic Updates — …/guides/optimistic-updates
  • TanStack Query v5: useMutation, useMutationState — …/reference/useMutation, …/reference/useMutationState
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
TanStack Query useMutation: serverga yozish va keshni yangilash — IlmHamroh