IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar25/36-dars21 daqiqa
Mundarija (25)

RTK Query: createApi, teglar bilan kesh yangilash va Redux loyihalarida server state

Qisqacha: RTK Query — Redux Toolkit ichidagi server state vositasi: TanStack Query'ning Redux'dagi "egizagi". createApi da baseQuery (odatda fetchBaseQuery) va endpoints yoziladi — build.query o'qish, build.mutation yozish uchun. Har endpoint uchun hook avtomatik yasaladi: useGetMenuQuery, useAddDishMutation. Kesh teglar bilan yangilanadi: so'rov providesTags: ["Menyu"] deydi, mutatsiya invalidatesTags: ["Menyu"] — shu tegli so'rovlar o'zi qayta yuklanadi. Kanon tajribasida (qo'lda yozilgan variantga nisbatan) RTK Query +24.8 KB, TanStack Query +10.3 KB: Redux loyihasida — RTK Query, Redux'siz loyihada — TanStack Query.

Bu darsda

  • createApi, fetchBaseQuery va endpoint'lar bilan API qatlami yozasiz.
  • Avtomatik hooklar (useGetMenuQuery) qanday nomlanishini va nima qaytarishini bilasiz.
  • providesTags/invalidatesTags bilan keshni yangilaysiz — umumiy teg va id li teg.
  • Token qo'shadigan va 401 da yangilaydigan baseQuery yozasiz.
  • Sahifalash va kod generatsiyasini (codegen) taniysiz.
  • RTK Query va TanStack Query'ni o'lchov bilan solishtirib, qaysi loyihada qaysi biri ekanini ayta olasiz.

Oldin bilishingiz kerak: Redux Toolkit II, TanStack Query II: kesh hayot sikli va kalitlar, TanStack Query III: mutatsiyalar, Server state vs client state.

1. Nega bu kerak?

O'tgan darsda menyuni createAsyncThunk bilan yukladik. Ishladi, lekin oxirida ro'yxat chiqqan edi: kesh, takroriy so'rovlarni birlashtirish, eskirish vaqti, bekor qilish — hammasini qo'lda yozish kerak. TanStack Query bularni tayyor beradi va vazifalar da aynan u ishlaydi.

Endi Sardorning bank loyihasini eslang: hammasi Redux'da. TanStack Query qo'shilsa, ilovada ikki "kesh" bo'ladi: biri Redux store'da, biri Query mijozida. DevTools ikkita, qoidalar ikkita. Redux jamoasi bu muammoga o'z javobini bergan — RTK Query. U Redux Toolkit paketining ichida, kesh — oddiy Redux holati, so'rovlar — oddiy Redux amallari.

Hayotiy o'xshatish: kutubxona kartotekasi. Kitob olgan har bir o'quvchi kartochkada belgilanadi (providesTags). Kitobning yangi nashri kelsa (invalidatesTags), kutubxonachi kartochkaga qaraydi va faqat shu kitobni olganlarga "yangisini olib keting" deydi. Hammaga emas.

2. Birinchi API

2.1 createApi va fetchBaseQuery

RTK Query alohida o'rnatilmaydi: @reduxjs/toolkit 2.13.0 ichida (@reduxjs/toolkit/query, React hooklari bilan — @reduxjs/toolkit/query/react). Saytdagi natija oynasida u yo'q. Shuning uchun avval Node'da, tarmoq o'rniga soxta fetch bilan ishga tushiramiz — so'rovlarni ko'rish uchun eng qulay yo'l:

js
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

// soxta server: tarmoq o'rniga (Node'da)
const dishes = [{ id: 1, name: "Osh" }];
globalThis.fetch = async (request) => {
  const url = new URL(request.url);
  console.log("so'rov:", request.method, url.pathname);
  if (request.method === "POST") {
    const body = await request.json();
    dishes.push({ id: dishes.length + 1, name: body.name });
    return Response.json(dishes.at(-1), { status: 201 });
  }
  return Response.json(dishes);
};

const menuApi = createApi({
  reducerPath: "menuApi",
  baseQuery: fetchBaseQuery({ baseUrl: "https://example.com/api/" }),
  tagTypes: ["Menyu"],
  endpoints: (build) => ({
    getMenu: build.query({
      query: () => "menyu",
      providesTags: ["Menyu"],
    }),
    addDish: build.mutation({
      query: (name) => ({
        url: "menyu",
        method: "POST",
        body: { name },
      }),
      invalidatesTags: ["Menyu"],
    }),
  }),
});

const store = configureStore({
  reducer: { [menuApi.reducerPath]: menuApi.reducer },
  middleware: (getDefault) =>
    getDefault().concat(menuApi.middleware),
});

const wait = (ms) => new Promise((r) => setTimeout(r, ms));
const select = menuApi.endpoints.getMenu.select();

const sub = store.dispatch(menuApi.endpoints.getMenu.initiate());
await sub;
console.log("kesh:", select(store.getState()).data);

// ikkinchi komponent ham so'radi — yangi so'rov yo'q
const sub2 = store.dispatch(menuApi.endpoints.getMenu.initiate());
await sub2;

await store.dispatch(menuApi.endpoints.addDish.initiate("Manti"));
await wait(50);
console.log("kesh:", select(store.getState()).data);
sub.unsubscribe();
sub2.unsubscribe();

Toza papkada @reduxjs/toolkit@2.13.0 bilan Node 24 da:

text
so'rov: GET /api/menyu
kesh: [ { id: 1, name: 'Osh' } ]
so'rov: POST /api/menyu
so'rov: GET /api/menyu
kesh: [ { id: 1, name: 'Osh' }, { id: 2, name: 'Manti' } ]

Qadamma-qadam:

Qadam Server Kesh (menuApi)
sub — getMenu GET [Osh]
sub2 — yana getMenu — [Osh] (keshdan)
addDish("Manti") POST "Menyu" tegi eskirdi
teg bo'yicha qayta yuklash GET [Osh, Manti]

Uchta muhim narsa:

  1. Bitta GET. getMenu ikki marta so'raldi (sub va sub2 — ikki komponent kabi), lekin server bir marta chaqirildi. Ikkinchisi keshdan oldi. TanStack Query'dagi bir xil queryKey bilan so'rovlar birlashishi (TanStack Query II) — bu yerda ham.
  2. Teg ishladi. addDish muvaffaqiyatli tugadi → uning invalidatesTags: ["Menyu"] i → providesTags: ["Menyu"] li getMenu o'zi qayta yuklandi (oxirgi GET). Keshda endi manti ham bor. Biz refetch ni hech qayerda chaqirmadik.
  3. Hammasi Redux. menuApi.reducer — store'dagi oddiy slice (menuApi kalitida), menuApi.middleware — keshning hayot siklini boshqaradi (obunalar, taymerlar, teglar). Ikkalasi ham ulanishi shart.

Qismlarga to'xtalamiz:

  • baseQuery — har so'rov o'tadigan umumiy funksiya. fetchBaseQuery — fetch ustidagi yupqa o'ram: baseUrl, JSON'ni o'zi o'qiydi, !res.ok bo'lsa xato qaytaradi (tashlamaydi). API qatlami darsidagi api.ts ning vazifasi.
  • endpoints — API'ning "menyusi". build.query — o'qish, build.mutation — yozish. query funksiyasi so'rovni tasvirlaydi: satr ("menyu" — baseUrl ga qo'shiladi) yoki { url, method, body } obyekti. body — obyekt bo'lsa, JSON'ga o'zi aylanadi.
  • tagTypes — ishlatiladigan teg nomlari ro'yxati.

2.2 Avtomatik hooklar

React loyihasida import @reduxjs/toolkit/query/react dan bo'ladi va createApi har endpoint uchun hook yasaydi. Nomi qoida bo'yicha: use + endpoint nomi (bosh harf bilan) + Query/Mutation. Kanon tajribasidagi variant (aynan):

tsx
// server-rtkq.tsx — RTK Query (Redux Toolkit 2): createApi, teglar bilan
// invalidatsiya. Redux store va Provider majburiy
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react";
import { Provider } from "react-redux";
import { ishgaTushir } from "./_umumiy.tsx";
import { Korinish, URL_ } from "./_server.tsx";
import type { Royxat, Vazifa } from "./_server.tsx";

const vazifalarApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: URL_ }),
  tagTypes: ["Vazifalar"],
  endpoints: (b) => ({
    royxat: b.query<Royxat, void>({ query: () => "", providesTags: ["Vazifalar"] }),
    qosh: b.mutation<Vazifa, string>({ query: (matn) => ({ url: "", method: "POST", body: { matn } }), invalidatesTags: ["Vazifalar"] }),
  }),
});
const store = configureStore({
  reducer: { [vazifalarApi.reducerPath]: vazifalarApi.reducer },
  middleware: (m) => m().concat(vazifalarApi.middleware),
});

function Ilova() {
  const { data, isLoading, error } = vazifalarApi.useRoyxatQuery();
  const [qosh] = vazifalarApi.useQoshMutation();
  return (
    <Korinish
      vazifalar={data?.vazifalar}
      yuklanmoqda={isLoading}
      xato={error ? JSON.stringify(error) : null}
      onQosh={(matn) => void qosh(matn)}
    />
  );
}
ishgaTushir(
  <Provider store={store}>
    <Ilova />
  </Provider>,
);
  • vazifalarApi.useRoyxatQuery() — royxat endpoint'i uchun. Qaytaradi: data, isLoading (birinchi yuklash), isFetching (har yuklash), error, refetch. TanStack Query'dagi useQuery natijasiga juda o'xshash.
  • vazifalarApi.useQoshMutation() — [funksiya, holat] juftligi: const [qosh, { isLoading }] = …. qosh(matn) promise qaytaradi; .unwrap() bilan natija yoki xatoni oddiy promise kabi olasiz.
  • Provider — oddiy react-redux Provider'i (Redux Toolkit I). Redux loyihasida u allaqachon bor.

Bu variant «Bahor» mashq API'si bilan Chrome 155 da ishlatildi (natijalar/tajriba/16-17-server-ishlaydi.txt):

text
Chrome: Chrome/155.0.8059.40 | variant: server-rtkq
ro'yxat: ["Tajriba vazifa"]
so'rovlar: GET tajriba-dgcvgb4crev, POST tajriba-dgcvgb4crev, OPTIONS tajriba-dgcvgb4crev, GET tajriba-dgcvgb4crev

GET → POST → GET: yuklash, qo'shish, teg bo'yicha qayta yuklash. O'rtadagi OPTIONS — brauzerning CORS preflight so'rovi (POST JSON boshqa domenga ketdi), RTK Query'ga aloqasi yo'q. TanStack Query, SWR va qo'lda yozilgan variantlarda ham aynan shu to'rt so'rov.

2.3 Xuddi shu g'oya — jonli, TanStack Query bilan

RTK Query natija oynasida yo'q, lekin uning g'oyasi — "yozuvdan keyin tegishli so'rov o'zi qayta yuklansin" — TanStack Query'da ham bor va u saytda ishlaydi. Teg o'rnida — queryKey, invalidatesTags o'rnida — invalidateQueries:

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

// soxta server (natija oynasida tarmoq yo'q)
const dishes = ["Osh"];
const delay = (ms) => new Promise((r) => setTimeout(r, ms));
async function getMenu() {
  console.log("GET /menyu");
  await delay(200);
  return [...dishes];
}
async function addDish(name) {
  console.log("POST /menyu");
  await delay(200);
  dishes.push(name);
}

function Menu() {
  const client = useQueryClient();
  const { data = [] } = useQuery({
    queryKey: ["menyu"],
    queryFn: getMenu,
  });
  const add = useMutation({
    mutationFn: addDish,
    onSuccess: () =>
      client.invalidateQueries({ queryKey: ["menyu"] }),
  });
  return (
    <>
      <p>Menyu: {data.join(", ")}</p>
      <button onClick={() => add.mutate("Manti")}>Manti qo'sh</button>
    </>
  );
}

const client = new QueryClient();

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

Konsolda:

text
GET /menyu
POST /menyu
GET /menyu

Ekranda:

text
Menyu: Osh, Manti
Manti qo'sh

Ketma-ketlik Node'dagi RTK Query bilan bir xil. Farq — kim bog'laydi: TanStack Query'da mutatsiya o'zi aytadi "qaysi kalitlarni yangila" (onSuccess ichida). RTK Query'da esa ikkala tomon e'lon qiladi: so'rov "men nimani beraman", mutatsiya "men nimani eskirtiraman" — bog'lanishni kutubxona topadi.

TanStack Query RTK Query
so'rov kaliti queryKey: ["menyu"] endpoint + argument: getPage(1)
yozuvdan keyin yangilash invalidateQueries({ queryKey }) invalidatesTags ↔ providesTags
kesh qayerda QueryClient Redux store (menuApi slice)
hooklar useQuery, useMutation (umumiy) useGetMenuQuery (har endpoint uchun)

Tekshirib ko'ring: addDish endpoint'ida invalidatesTags yozilmasa, Node misolidagi so'rovlar ro'yxati qanday bo'ladi?

Javob

GET, keyin POST — va boshqa hech narsa. Mutatsiya serverda manti qo'shadi, lekin keshdagi ro'yxat eski qoladi: oxirgi kesh: qatorida faqat osh. Ro'yxat qayta yuklanishi uchun nimadir uni eskirtirishi kerak — teg, refetch() yoki keepUnusedDataFor muddati tugashi va qayta obuna.

3. Teglar chuqurroq

3.1 id li teglar

["Menyu"] — umumiy teg: har qanday o'zgarish hamma menyu so'rovlarini qayta yuklaydi. 300 taomli menyuda bitta narx o'zgarsa, 300 ta taom sahifasi qayta so'ralishi kerak emas. Teg aniqroq bo'lishi mumkin — { type, id }:

js
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

const db = {
  1: { id: 1, name: "Osh", price: 35000 },
  2: { id: 2, name: "Manti", price: 30000 },
};
globalThis.fetch = async (request) => {
  const id = new URL(request.url).pathname.split("/").at(-1);
  console.log("so'rov:", request.method, id);
  if (request.method === "PATCH") {
    Object.assign(db[id], await request.json());
  }
  return Response.json(db[id]);
};

const menuApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "https://example.com/api/" }),
  tagTypes: ["Taom"],
  endpoints: (build) => ({
    getDish: build.query({
      query: (id) => `taom/${id}`,
      providesTags: (result, error, id) => [{ type: "Taom", id }],
    }),
    setPrice: build.mutation({
      query: ({ id, price }) => ({
        url: `taom/${id}`,
        method: "PATCH",
        body: { price },
      }),
      invalidatesTags: (result, error, { id }) => [
        { type: "Taom", id },
      ],
    }),
  }),
});

const store = configureStore({
  reducer: { [menuApi.reducerPath]: menuApi.reducer },
  middleware: (getDefault) =>
    getDefault().concat(menuApi.middleware),
});

const { getDish, setPrice } = menuApi.endpoints;
store.dispatch(getDish.initiate(1));
store.dispatch(getDish.initiate(2));
await new Promise((r) => setTimeout(r, 50));

console.log("--- 1-taom narxi o'zgaradi");
await store.dispatch(setPrice.initiate({ id: 1, price: 38000 }));
await new Promise((r) => setTimeout(r, 50));
console.log(getDish.select(1)(store.getState()).data.price);

Node 24, RTK 2.13.0:

text
so'rov: GET 1
so'rov: GET 2
--- 1-taom narxi o'zgaradi
so'rov: PATCH 1
so'rov: GET 1
38000

providesTags endi funksiya: (natija, xato, argument) => teglar. Har taom so'rovi o'z id sini beradi: { type: "Taom", id: 1 }. Mutatsiya faqat { type: "Taom", id: 1 } ni eskirtirdi — 2-taom qayta so'ralmadi (PATCH dan keyin faqat GET 1).

Ro'yxat va bitta yozuvni birga ishlatish uchun odatiy naqsh:

js
getMenu: build.query({
  query: () => "menyu",
  providesTags: (result = []) => [
    ...result.map(({ id }) => ({ type: "Taom", id })),
    { type: "Taom", id: "RO'YXAT" },
  ],
}),
addDish: build.mutation({
  query: (body) => ({ url: "menyu", method: "POST", body }),
  // yangi taom — ro'yxat o'zgardi, mavjud taomlar emas
  invalidatesTags: [{ type: "Taom", id: "RO'YXAT" }],
}),

"RO'YXAT" — maxsus id: "butun ro'yxat" degan teg. Qo'shish ro'yxatni, tahrir — bitta taomni (va u ro'yxatda ham bor — ro'yxat ham yangilanadi, chunki unda shu id li teg bor).

3.2 Optimistik yangilash: onQueryStarted

Teglar "server javobidan keyin qayta yukla" deydi. Ba'zan javobni kutmasdan ekranni darhol yangilash kerak — useOptimistic darsidagi g'oya. RTK Query'da: mutatsiyaning onQueryStarted ida keshni qo'lda tuzatish (updateQueryData — Immer qoralamasi bilan) va xato bo'lsa — patch.undo(). Buni 3-mashqda yozasiz.

4. Auth: token va 401

4.1 prepareHeaders

Himoyalangan API har so'rovda token kutadi. fetchBaseQuery ning prepareHeaders i har so'rovdan oldin chaqiriladi va sarlavhalarni to'ldiradi. Token Redux holatida bo'lsa — getState() bilan olinadi.

Diqqat: bu misoldagi token — o'quv uchun soxta satr. Token qayerda saqlanishi (xotira, httpOnly cookie), qanday yangilanishi va serverda tekshirilishi — Autentifikatsiyali so'rovlar va 28-qismdagi haqiqiy auth mavzusi. localStorage dagi token XSS bilan o'g'irlanishi mumkin.

4.2 401 da yangilash: baseQuery ni o'rash

Token eskirsa, server 401 qaytaradi. Har endpoint'da buni tekshirmaslik uchun baseQuery ning o'zini o'raymiz: xato 401 bo'lsa — yangi token so'raymiz va asl so'rovni qaytaramiz:

js
import { configureStore, createSlice } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

// soxta server: "eski-token" muddati o'tgan, "yangi-token" ishlaydi
globalThis.fetch = async (request) => {
  const url = new URL(request.url);
  const auth = request.headers.get("authorization");
  console.log("so'rov:", url.pathname, auth);
  if (url.pathname === "/api/yangila") {
    return Response.json({ token: "yangi-token" });
  }
  if (auth !== "Bearer yangi-token") {
    return Response.json({ xato: "token eskirgan" }, { status: 401 });
  }
  return Response.json([{ id: 12, total: 63000 }]);
};

const authSlice = createSlice({
  name: "auth",
  initialState: { token: "eski-token" },
  reducers: {
    tokenReceived(state, action) {
      state.token = action.payload;
    },
  },
});

const rawBaseQuery = fetchBaseQuery({
  baseUrl: "https://example.com/api/",
  prepareHeaders: (headers, { getState }) => {
    const { token } = getState().auth;
    if (token) headers.set("authorization", `Bearer ${token}`);
    return headers;
  },
});

const baseQueryWithReauth = async (args, api, extra) => {
  let result = await rawBaseQuery(args, api, extra);
  if (result.error?.status === 401) {
    const refresh = await rawBaseQuery("yangila", api, extra);
    if (refresh.data) {
      const { tokenReceived } = authSlice.actions;
      api.dispatch(tokenReceived(refresh.data.token));
      result = await rawBaseQuery(args, api, extra);
    }
  }
  return result;
};

const ordersApi = createApi({
  baseQuery: baseQueryWithReauth,
  endpoints: (build) => ({
    getOrders: build.query({ query: () => "buyurtmalar" }),
  }),
});

const store = configureStore({
  reducer: {
    auth: authSlice.reducer,
    [ordersApi.reducerPath]: ordersApi.reducer,
  },
  middleware: (getDefault) =>
    getDefault().concat(ordersApi.middleware),
});

const result = await store.dispatch(
  ordersApi.endpoints.getOrders.initiate(),
);
console.log("natija:", result.data);
console.log("token:", store.getState().auth.token);

Node 24, RTK 2.13.0:

text
so'rov: /api/buyurtmalar Bearer eski-token
so'rov: /api/yangila Bearer eski-token
so'rov: /api/buyurtmalar Bearer yangi-token
natija: [ { id: 12, total: 63000 } ]
token: yangi-token

Birinchi so'rov eski token bilan — 401. O'ram buni ko'rib, /yangila ga murojaat qildi, yangi tokenni Redux'ga yozdi (tokenReceived) va asl so'rovni qayta yubordi — endi prepareHeaders yangi tokenni qo'ydi. Komponent bularning hech birini bilmaydi: u faqat natijani oldi.

Token yangilash navbati darsidagi muammo bu yerda ham bor: bir vaqtda o'nta so'rov 401 olsa, o'nta yangilash ketmasin. Shuning uchun haqiqiy loyihada o'ram ichida "yangilash ketyapti" belgisini (masalan, async-mutex kutubxonasi bilan qulf) saqlashadi.

5. Sahifalash va codegen

5.1 Har sahifa — alohida kesh yozuvi

RTK Query'da so'rov kaliti (hujjatda — cache key) — endpoint nomi va argument. Sahifa raqamini argument qilsangiz, har sahifa o'z yozuviga tushadi:

js
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

const all = ["Osh", "Lag'mon", "Manti", "Ko'k choy"];
globalThis.fetch = async (request) => {
  const url = new URL(request.url);
  console.log("so'rov:", url.search);
  const page = Number(url.searchParams.get("sahifa"));
  return Response.json(all.slice((page - 1) * 2, page * 2));
};

const menuApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "https://example.com/api/" }),
  endpoints: (build) => ({
    getPage: build.query({
      query: (page) => `menyu?sahifa=${page}&hajm=2`,
    }),
  }),
});

const store = configureStore({
  reducer: { [menuApi.reducerPath]: menuApi.reducer },
  middleware: (getDefault) =>
    getDefault().concat(menuApi.middleware),
});

const { getPage } = menuApi.endpoints;
console.log((await store.dispatch(getPage.initiate(1))).data);
console.log((await store.dispatch(getPage.initiate(2))).data);
console.log((await store.dispatch(getPage.initiate(1))).data);
console.log(Object.keys(store.getState().api.queries));

Node 24, RTK 2.13.0:

text
so'rov: ?sahifa=1&hajm=2
[ 'Osh', "Lag'mon" ]
so'rov: ?sahifa=2&hajm=2
[ 'Manti', "Ko'k choy" ]
[ 'Osh', "Lag'mon" ]
[ 'getPage(1)', 'getPage(2)' ]

Uchinchi chaqiruv — 1-sahifa yana: so'rov yo'q, keshdan. Oxirgi qatorda kesh kalitlari: getPage(1), getPage(2) — argument kalitning bir qismi. React'da: useGetPageQuery(page) — page state o'zgarsa, yangi sahifa yuklanadi, eskisiga qaytsangiz — darhol. Yangi sahifa yuklanayotganda eskisini ko'rsatib turish uchun isFetching (yuklanmoqda) va data (oxirgi muvaffaqiyatli natija) ni birga ishlatasiz.

"Yana yuklash" (cheksiz ro'yxat) uchun Redux Toolkit 2.6 dan beri alohida build.infiniteQuery bor — TanStack Query'dagi useInfiniteQuery (TanStack Query IV) ning analogi: getNextPageParam va sahifalar massivi.

5.2 Codegen: endpoint'larni OpenAPI'dan yasash

Katta API'da 200 ta endpoint bo'lsa, ularni qo'lda yozish — xato manbai. Agar backend OpenAPI (Swagger) hujjati bersa — har endpoint, parametr va javob turi tasvirlangan JSON fayl — RTK Query uchun kodni generator yasaydi:

bash
npx @rtk-query/codegen-openapi openapi-config.ts
ts
// openapi-config.ts — generator sozlamasi (namuna)
import type { ConfigFile } from "@rtk-query/codegen-openapi";

const config: ConfigFile = {
  schemaFile: "https://example.com/api/openapi.json",
  apiFile: "./src/store/emptyApi.ts",
  apiImport: "emptySplitApi",
  outputFile: "./src/store/bahorApi.ts",
  exportName: "bahorApi",
  hooks: true,
};

export default config;

Natija — bahorApi.ts: endpoint'lar, TypeScript turlari va hooklar. Backend o'zgarsa, buyruqni qayta ishga tushirasiz va tur xatolari qayerni tuzatish kerakligini ko'rsatadi. Bu — type-safe kontrakt g'oyasining avtomatik varianti. Biz uni ishga tushirmadik: «Bahor» mashq API'sida OpenAPI hujjati yo'q.

Tekshirib ko'ring: useGetPageQuery(page) da page 1 → 2 → 1 bo'lsa, nechta so'rov ketadi (kesh muddati tugamagan bo'lsa)?

Javob

Ikkita: getPage(1) va getPage(2). Uchinchi o'tish keshdagi getPage(1) ni oladi. Sukut bo'yicha foydalanilmayotgan yozuv 60 soniya saqlanadi (keepUnusedDataFor); undan keyin qaytsangiz — yangi so'rov.

6. Kanon tajribasi: to'rt yo'l, bir vazifa (nega kanonga kirmadi)

vazifalar da server state TanStack Query'da qoladi (holat 14, TanStack Query I–III). Bu qaror to'rt variantni bir xil vazifada sinab ko'rib qabul qilingan: ro'yxatni serverdan yuklash, POST bilan qo'shish, ro'yxatni yangilash. To'rtalasi «Bahor» API'si bilan Chrome 155 da ishladi (yuqoridagi 16-17-server-ishlaydi.txt — hamma variantda GET, POST, OPTIONS, GET). Hajmi (tajriba/hajm.txt, React asosi bilan):

Server state: bir xil vazifa, to'rt variant (gzip, React bilan)
  • qo'lda (fetch + useState)68 001 B, 35 qator66,4 KB
  • SWR 2.5.173 381 B, 16 qator71,7 KB
  • TanStack Query 5.104.178 333 B, 21 qator76,5 KB
  • RTK Query (RTK 2.13.0)92 813 B, 35 qator90,6 KB

Manba: IlmHamroh kurs tajribasi (Vite 8.3.3, gzip -9, 2026-10-13)

Qo'lda yozilgan variantga nisbatan: SWR +5.4 KB, TanStack Query +10.3 KB, RTK Query +24.8 KB. (vazifalar ning o'zida Query +9.9 KB chiqqan: u yerda qo'lda yozilgan kodning bir qismi o'chirildi — TanStack Query I.) RTK Query'ning katta qismi — Redux'ning o'zi: configureStore, react-redux, Immer. Redux allaqachon bor loyihada bu narx to'lab bo'lingan — RTK Query'ning o'zi qo'shadigani ancha kichik.

Nega kanonga kirmadi:

  • vazifalar Redux'da emas. RTK Query uchun Redux'ni olib kirish kerak bo'lardi: +24.8 KB va ikkinchi holat tizimi (client state Zustand'da).
  • TanStack Query kichikroq va 18-qismdagi qo'lda kodni (pult, "kutilmoqda", tarmoq holati) bitta queryKey ga almashtirdi: 7 qadamda 4 GET → 2 GET (TanStack Query II dagi o'lchov).
  • Optimistik qatlam kanonda React'ning useOptimistic ida — ikkala kutubxona bilan ham bir xil ishlaydi.

Qachon RTK Query — to'g'ri tanlov:

  • Loyiha Redux'da. Bitta store, bitta DevTools, so'rovlar ham Redux amallari sifatida jurnalda. Ikkinchi kesh kutubxonasini qo'shmaysiz.
  • Ko'p endpoint'li API va OpenAPI hujjati bor — codegen bilan butun qatlam generatsiya qilinadi.
  • Server ma'lumoti va client state bir-biriga chambarchas: listener middleware (Redux Toolkit II) RTK Query amallariga ham quloq soladi (masalan, getMenu.matchFulfilled).

SWR — eng kichik, oddiy "yukla va keshla" uchun yaxshi. Lekin mutatsiyalar va bekor qilishda TanStack Query'dan kamroq imkoniyat beradi (TanStack Query V darsida taqqoslangan).

7. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Warning: Middleware for RTK-Query API at reducerPath "api" has not been added to the store. api.middleware ulanmagan getDefault().concat(api.middleware)
hook undefined yoki "not a function" endpoint nomi va hook nomi mos emas getMenu → useGetMenuQuery
mutatsiyadan keyin ro'yxat eski providesTags/invalidatesTags yo'q yoki teg nomlari farq qiladi ikkala tomonda bir xil teg; tagTypes da e'lon
bitta o'zgarish hamma narsani qayta yuklaydi faqat umumiy teg (["Taom"]) { type: "Taom", id }
ikki createApi — ikki kesh, teglar ishlamaydi har fayl o'z API'sini yaratgan bitta createApi + injectEndpoints
401 dan keyin hamma so'rov yiqiladi token yangilash yo'q baseQuery o'rami (401 → yangilash → qayta)
isLoading yangi sahifada false, lekin ma'lumot eski isLoading — faqat birinchi yuklash isFetching ni tekshiring

8. Mashqlar

Mashqlar kurs/mashqlar/19/25-rtk-query/ da. 2- va 3-mashq Node'da (o'z papkangizda @reduxjs/toolkit@2.13.0, .mjs); soxta fetch misollardagidek.

1-mashq (oson): teglarni qo'ying

Mutatsiyadan keyin getOrders o'zi qayta yuklanishi uchun bo'sh joylarni to'ldiring:

js
const ordersApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "/api/" }),
  tagTypes: ["Buyurtma"],
  endpoints: (build) => ({
    getOrders: build.query({
      query: () => "buyurtmalar",
      [___:providesTags]: ["Buyurtma"],
    }),
    addOrder: build.mutation({
      query: (body) => ({ url: "buyurtmalar", method: "POST", body }),
      [___:invalidatesTags]: ["Buyurtma"],
    }),
  }),
});

Avtomatik hooklarning nomlari: [:useGetOrdersQuery] va [:useAddOrderMutation].

Yechim

providesTags — so'rov "men shu tegli ma'lumotni beraman" deydi; invalidatesTags — mutatsiya "shu teg eskirdi" deydi. Nomlar: use + GetOrders + Query → useGetOrdersQuery; use + AddOrder + Mutation → useAddOrderMutation.

2-mashq (o'rta): faqat o'zgargan taom

«id li teglar» bo'limidagi misolni o'zingiz yozing va tekshiring: ikki taom yuklanadi, 1-taom narxi o'zgaradi — qayta faqat GET 1 ketsin, GET 2 emas. Oxirida 1-taomning yangi narxini chiqaring.

Yechim

Yechim — «id li teglar» bo'limidagi kodning o'zi (kurs/mashqlar/19/25-rtk-query/2-yechim.mjs). Natija:

text
so'rov: GET 1
so'rov: GET 2
--- 1-taom narxi o'zgaradi
so'rov: PATCH 1
so'rov: GET 1
38000

Agar invalidatesTags: ["Taom"] (umumiy) yozsangiz, PATCH dan keyin ikkita GET ketadi — GET 1 va GET 2. Tekshirib ko'ring: farq sonda ko'rinadi.

3-mashq (qiyin): optimistik savat

Savat (getCart, boshida ["Osh"]) va addItem(name) mutatsiyasi. Server "Lag'mon" ni rad etadi (500). Talab:

  1. onQueryStarted da taomni keshga darhol qo'shing (cartApi.util.updateQueryData) va darhol: <nom> deb yozing.
  2. await queryFulfilled xato bersa — patch.undo() va qaytarildi: <nom>.
  3. Manti va Lag'mon qo'shilgandan keyin keshni chiqaring.

Ishora: getCart serverga bormasin — queryFn: () => ({ data: ["Osh"] }) (so'rov o'rniga tayyor qiymat).

Yechim
js
import { configureStore } from "@reduxjs/toolkit";
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query";

// soxta server: "Lag'mon" ni saqlashni rad etadi
globalThis.fetch = async (request) => {
  const { name } = await request.json();
  if (name === "Lag'mon") {
    return Response.json({ xato: "band" }, { status: 500 });
  }
  return Response.json({ name }, { status: 201 });
};

const cartApi = createApi({
  baseQuery: fetchBaseQuery({ baseUrl: "https://example.com/api/" }),
  endpoints: (build) => ({
    getCart: build.query({ queryFn: () => ({ data: ["Osh"] }) }),
    addItem: build.mutation({
      query: (name) => ({
        url: "savat",
        method: "POST",
        body: { name },
      }),
      async onQueryStarted(name, { dispatch, queryFulfilled }) {
        const add = (draft) => {
          draft.push(name);
        };
        const patch = dispatch(
          cartApi.util.updateQueryData("getCart", undefined, add),
        );
        console.log("darhol:", name);
        try {
          await queryFulfilled;
        } catch {
          patch.undo();
          console.log("qaytarildi:", name);
        }
      },
    }),
  }),
});

const store = configureStore({
  reducer: { [cartApi.reducerPath]: cartApi.reducer },
  middleware: (getDefault) =>
    getDefault().concat(cartApi.middleware),
});

const { getCart, addItem } = cartApi.endpoints;
await store.dispatch(getCart.initiate());
await store.dispatch(addItem.initiate("Manti"));
await store.dispatch(addItem.initiate("Lag'mon"));
console.log(getCart.select()(store.getState()).data);

Node 24, RTK 2.13.0:

text
darhol: Manti
darhol: Lag'mon
qaytarildi: Lag'mon
[ 'Osh', 'Manti' ]

Lag'mon ham bir lahza keshda bo'ldi ("darhol"), server rad etgach — olib tashlandi. Bekor qilishda patch.undo() Immer yozib qo'ygan teskari o'zgarishni qo'llaydi. Shuning uchun bu oraliqda kelgan boshqa o'zgarishlar (masalan, manti) buzilmaydi. useOptimistic dagi g'oya, faqat kesh Redux'da.

4-mashq (tahlil): qaysi biri?

Har loyiha uchun tanlang — RTK Query, TanStack Query yoki SWR — va bir gap bilan asoslang:

  1. Bank ilovasi, 5 yillik, Redux'da, 180 ta REST endpoint, backend OpenAPI beradi.
  2. «Bahor» sayti: Vite + React, holat Zustand'da, 6 ta endpoint, optimistik savat.
  3. Blog: faqat maqolalarni o'qish, yozish yo'q, hajm har kilobaytda muhim.
Yechim
  1. RTK Query — Redux allaqachon bor (narxi to'langan), codegen 180 endpoint'ni yasaydi, bitta store va DevTools.
  2. TanStack Query — Redux yo'q; +10.3 KB, mutatsiya va optimistik yangilash qulay. Bu aynan vazifalar qarori.
  3. SWR — eng kichik (+5.4 KB), faqat o'qish uchun yetarli. TanStack Query ham xato emas, lekin bu yerda ortiqcha.

9. Real ishda

  • Redux loyihalarida yangi server kodining standarti — RTK Query; eski thunk + status kodlari bosqichma-bosqich unga ko'chiriladi.
  • injectEndpoints — katta ilovada bitta createApi (bitta kesh) va har modul o'z endpoint'larini alohida faylda "ulaydi". Codegen ham shu usulda ishlaydi (emptySplitApi).
  • Monitoring: RTK Query amallari (api/executeQuery/pending…) Redux DevTools'da ko'rinadi — so'rovlar tarixi holat tarixi bilan bir joyda.
  • Intervyu savollari: "RTK Query va TanStack Query farqi?", "teglar qanday ishlaydi?", "token eskirganda qayta so'rashni qanday qilasiz?", "isLoading va isFetching farqi?".

Xulosa

  • RTK Query — Redux Toolkit ichidagi server state vositasi: createApi + baseQuery + endpoints; reducer va middleware store'ga ulanadi.
  • Har endpoint'ga avtomatik hook: useGetMenuQuery, useAddDishMutation; bir xil so'rovlar birlashadi, natija keshda.
  • Teglar: so'rov providesTags, mutatsiya invalidatesTags; aniq yangilash uchun { type, id }.
  • Auth — prepareHeaders va 401 da yangilaydigan baseQuery o'rami; sahifalash — argument so'rov kalitining bir qismi; codegen — OpenAPI'dan.
  • Tajriba: RTK Query +24.8 KB (Redux bilan), TanStack Query +10.3 KB, SWR +5.4 KB. Redux loyihasida — RTK Query, vazifalar kabi Redux'siz loyihada — TanStack Query.

Keyingi dars: Atom va proxy state: Jotai, Valtio, MobX — holatni mayda atomlarga bo'lish, "o'zgartirib yoz" proxy modeli va signals g'oyasi; tajriba o'lchovlari bilan.

Manbalar

  • Redux Toolkit: RTK Query Overview, createApi, fetchBaseQuery — redux-toolkit.js.org/rtk-query (v2.13, 2026-10 holatiga)
  • RTK Query: Automated Re-fetching (tags), Manual Cache Updates (onQueryStarted, updateQueryData) — redux-toolkit.js.org/rtk-query/usage
  • RTK Query: Customizing queries (re-authorization), Pagination, Infinite Queries, Code Generation — redux-toolkit.js.org/rtk-query/usage
  • TanStack Query: Comparison (React Query vs RTK Query vs SWR) — tanstack.com/query/latest/docs/framework/react/comparison
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
RTK Query: createApi, teglar bilan kesh yangilash va Redux loyihalarida server state — IlmHamroh