Mundarija (32)
- Bu darsda
- 1. Nega bu kerak?
- 2. useMutation asoslari
- 2.1 Birinchi yozuv
- 2.2 Mutatsiya holati
- 2.3 mutate va mutateAsync
- 3. Callback'lar: onSuccess, onError, onSettled
- 3.1 Tartib
- 3.2 Callback qayerda: hook'dami yoki mutate dami
- 4. Keshni yangilashning ikki yo'li
- 4.1 invalidateQueries — "qayta so'ra"
- 4.2 setQueryData — "o'zim yozaman"
- 4.3 Qaysi birini tanlash
- 5. Optimistik yangilash
- 5.1 variables orqali — eng oddiy usul
- 5.2 onMutate orqali — keshning o'zini o'zgartirish
- 5.3 Tuzoq: bir vaqtdagi ikki yozuv
- 6. Vazifalar qadami: yozish — useMutation, commit (holat 14)
- 6.1 Qaror: optimistik qatlam — React'da, tasdiq — Query'da
- 6.2 useYozuv va tasdiqniQoy (server-sinxron.ts)
- 6.3 App: useOptimistic endi tashqi store ustida
- 6.4 Vazifa sahifasi: action ham o'sha kesh
- 6.5 Testlar va commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): callback'lar tartibi
- 2-mashq (o'rta): o'chirish va kesh
- 3-mashq (qiyin): optimistik "yoqdi" va qaytarish
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
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) yokisetQueryData(javobni keshga o'zing yoz). Optimistik yangilashda ekran serverni kutmaydi: avval o'zgartiriladi, xato bo'lsa — qaytariladi.
Bu darsda
useMutationbilan serverga yozasiz:mutate,mutateAsync,isPending,isError,variables.onSuccess,onError,onSettledqachon chaqirilishini tartib bilan ko'rasiz.- Yozuvdan keyin keshni
invalidateQueriesyokisetQueryDatabilan yangilaysiz va qaysi biri qachon to'g'ri ekanini bilasiz. - Optimistik yangilashning ikki usulini yozasiz:
variablesorqali vaonMutate+ qaytarish. - Bir vaqtdagi ikki yozuvda snapshot qaytarish nega xato berishini ko'rasiz.
vazifalardagi 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:
- O'zi ishga tushmaydi.
useQuerykomponent chiqqanda so'raydi.useMutationesa sizmutate(...)ni chaqirguncha jim. - Kalit shart emas. Yozuv keshga tushmaydi, u keshni o'zgartiradi.
- 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:
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:
GET
POST Lag'mon
GETEkranda:
Osh, Manti, Lag'mon
Qo'shishKetma-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.awaitqilsa 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 dataTartibni konsolda ko'ramiz. Ikkinchi bosish ataylab xato beradi:
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:
onMutate: Osh
mutationFn: Osh
onSuccess: Osh qabul qilindi
onSettled
onMutate: Shashlik
mutationFn: Shashlik
onError: Shashlik tugadi
onSettledEkranda:
Osh Shashlik
error: Shashlik tugadionSuccess 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. mutatedagi — 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:
onSuccessdainvalidateQueries,onSettledda haminvalidateQueriesyozildi. 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):
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:
GET
POST Lag'monEkranda:
1. Osh
2. Lag'mon
Qo'shishIkkinchi GET yo'q. Yangi taom serverning javobidan (id: 2 bilan) to'g'ridan-to'g'ri keshga tushdi.
Diqqat:
setQueryDatada 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:
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:
Osh
Manti
Lag'mon
Qo'shishBosish 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:
cancelQueries— yo'ldagi GET'larni bekor qilish. Aks holda eski javob kelib, optimistik o'zgarishni bosib ketadi.- Snapshot —
getQueryDatabilan hozirgi qiymatni eslab qolish. setQueryData— optimistik o'zgarish. SnapshotonMutatedan qaytariladi.onError— snapshot'ni qaytarish.onSettled— server bilan solishtirish.
Server bu safar rad etadi:
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:
optimistik: Shashlik
qaytarildi: Shashlik qo'shilmadi: ruxsat yo'qEkranda:
Osh, Manti
Shashlik qo'shish
Shashlik qo'shilmadi: ruxsat yo'q
Jami: 2 taBosish 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?
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:
qaytarildi: Osh, SomsaEkranda:
Osh, Somsa, Manti — Jami: 3 ta
Lag'mon MantiOxiri 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
useOptimistici shunday ishlaydi: ekran = server tasdiqlagan ro'yxat + hali kutilayotgan amallar. Bitta amal rad etilsa, faqat u qatlamdan chiqadi, boshqalari qoladi.variablesusuli ham shu g'oyaga yaqin. - Oxirgi yozuvda invalidatsiya. Har
onSettledda 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:
onMutatedacancelQueriesni 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):
/**
* 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).
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:
mutationFnargumenti — funksiya (serverda).vazifalarda yozuv turlari ko'p: qo'shish, belgilash, tahrir, o'chirish, import. Har biriapining o'z metodini chaqiradi va server tasdiqlagan amalni qaytaradi. Bitta mutatsiya hammasiga xizmat qiladi.cancelQueries— boshida, «onMutateorqali» bo'limidagi birinchi qadam.onSuccess—setQueryData(tasdiqni qo'yish),onSettled—invalidateQueries.mutationKeyvaisMutating— "yo'lda nechta yozuv bor".=== 1— faqat shu yozuv qoldi, ya'ni u oxirgisi. 17-qismdagikutilmoqdahisobi shu.mutateAsync+try/catch— App'ningasyncaction'i natijani kutadi va xatoni e'longa aylantiradi.
6.3 App: useOptimistic endi tashqi store ustida
App.tsx da yozuv bitta qatorga qisqardi (kanon):
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):
+// 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):
// 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):
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:
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.
git add -A
git commitCommit (kanon): refactor: server holati — TanStack Query (useQuery, useMutation), bitta kesh sahifalar uchun.
Tekshiruvlar (haqiqiy, holat 14):
npm test— node:testℹ tests 174, VitestTests 56 passed (56). «Orqaga» tuzatishidan (React Router III) keyin kanonda — 182 (yangitarix-chuqurligi.test.jsdagi 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
/kirishtuzatishlaridan 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.tsdagi 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.
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:
Osh, Manti, Lag'mon
Manti ni o'chirishYechim
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:
GET
DELETE MantiEkranda:
Osh, Lag'mon
Manti ni o'chirishfilter 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:
- Kesh:
["likes"]→ son (boshida 10). onMutate—cancelQueries, snapshot, son +1.onError— snapshot'ni qaytarish va konsolgaqaytarildi: <son>.onSettled— invalidatsiya.- Ekranda: "Yoqdi: N" va tugma.
Server:
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
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:
qaytarildi: 11Ekranda:
Yoqdi: 11
YoqdiBirinchi 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=1bilan 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+invalidateQueriesning 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
onSubmiti ichidamutateAsync(RHF + Zod darsida). - Next.js da server tomondagi yozuv — Server Actions; klientdagi kesh baribir yangilanishi kerak (Next.js qismida).
- Intervyu savollari: "
invalidateQueriesvasetQueryDatafarqi?", "optimistik yangilashni qanday yozasiz va xatoda nima bo'ladi?", "mutatevamutateAsync?", "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→onSuccessyokionError→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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!