Redux Toolkit — createAsyncThunk, RTK Query

~25 daq · React · State Boshqarish

Muammodan boshlaylik

60-darsda reducer lar sof funksiya bo'lishi kerakligini aytdik: ichida fetch yo'q. Lekin ilovaga serverdan ma'lumot kerak. Qayerga yozamiz?

// ❌ reducer ichida — mumkin emas
reducers: {
  yukla: async (state) => {
    const j = await fetch('/api/mahsulotlar');   // reducer async bo'la olmaydi
    state.mahsulotlar = await j.json();
  },
}

Redux Toolkit da async mantiq uchun ikki vosita bor:

  • createAsyncThunk — reducer dan tashqarida async funksiya; natijasini action lar orqali reducer ga beradi
  • RTK Query — server ma'lumotlari uchun to'liq yechim (TanStack Query ga o'xshash)

Thunk nima

Thunk — action o'rniga dispatch qilinadigan funksiya. configureStore ga o'rnatilgan thunk middleware uni ushlaydi va chaqiradi:

// Oddiy action — obyekt
dispatch({ type: 'savat/tozalash' });

// Thunk — funksiya
dispatch(async (dispatch, getState) => {
  const j = await fetch('/api/mahsulotlar');
  const data = await j.json();
  dispatch(mahsulotlarKeldi(data));       // natijani oddiy action bilan yuborish
});

Thunk ichida: async kod, dispatch (bir yoki bir necha marta), getState(). Reducer lar esa sof qoladi.


createAsyncThunk

Har bir so'rovda uch holat bor: yuklanmoqda, muvaffaqiyat, xato (49-dars). createAsyncThunk ular uchun action larni avtomatik yaratadi:

// features/mahsulotlar/mahsulotlarSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

export const mahsulotlarniYukla = createAsyncThunk(
  'mahsulotlar/yukla',                          // action type prefiksi
  async (kategoriya, { signal, rejectWithValue }) => {
    const j = await fetch(`/api/mahsulotlar?kategoriya=${kategoriya}`, { signal });
    if (!j.ok) return rejectWithValue(`Server xatosi: ${j.status}`);
    return j.json();                            // → action.payload
  }
);

Bitta createAsyncThunk uchta action type yaratadi:

Action Qachon payload
mahsulotlar/yukla/pending So'rov boshlandi —
mahsulotlar/yukla/fulfilled Muvaffaqiyat return qilingan qiymat
mahsulotlar/yukla/rejected Xato rejectWithValue(...) qiymati; action.error da xato

extraReducers — thunk natijalarini ushlash

const mahsulotlarSlice = createSlice({
  name: 'mahsulotlar',
  initialState: {
    royxat: [],
    holat: 'idle',           // 'idle' | 'yuklanmoqda' | 'muvaffaqiyat' | 'xato'
    xato: null,
  },
  reducers: {},
  extraReducers: (builder) => {
    builder
      .addCase(mahsulotlarniYukla.pending, (state) => {
        state.holat = 'yuklanmoqda';
        state.xato = null;
      })
      .addCase(mahsulotlarniYukla.fulfilled, (state, action) => {
        state.holat = 'muvaffaqiyat';
        state.royxat = action.payload;
      })
      .addCase(mahsulotlarniYukla.rejected, (state, action) => {
        if (action.meta.aborted) return;          // bekor qilingan — xato emas
        state.holat = 'xato';
        state.xato = action.payload ?? action.error.message;
      });
  },
});
  • reducers — slice ning o'z action lari
  • extraReducers — tashqaridan keladigan action lar (thunk lar, boshqa slice lar)

Komponentda

function Mahsulotlar({ kategoriya }) {
  const dispatch = useDispatch();
  const { royxat, holat, xato } = useSelector((s) => s.mahsulotlar);

  useEffect(() => {
    const promise = dispatch(mahsulotlarniYukla(kategoriya));
    return () => promise.abort();                // race condition dan himoya (50-dars)
  }, [kategoriya, dispatch]);

  if (holat === 'yuklanmoqda') return <Skeleton />;
  if (holat === 'xato') return <p role="alert">{xato}</p>;

  return <ul>{royxat.map((m) => <li key={m.id}>{m.nomi}</li>)}</ul>;
}

dispatch(thunk) qaytargan promise ning .abort() metodi bor — u signal orqali fetch ni to'xtatadi.

[!INFO] useSelector((s) => s.mahsulotlar) bu yerda xavfsiz: u store dagi mavjud obyektni qaytaradi, yangisini yaratmaydi. Obyekt faqat slice o'zgarganda yangilanadi.

Natijani komponentda kutish — unwrap

async function saqla() {
  try {
    const yangi = await dispatch(mahsulotQosh(forma)).unwrap();
    navigate(`/mahsulot/${yangi.id}`);
  } catch (xato) {
    toast.error(xato);
  }
}

.unwrap() — fulfilled da payload ni qaytaradi, rejected da throw qiladi.


Muammo: bu ko'p kod

Bitta ro'yxatni yuklash uchun: thunk, uchta holat, uchta addCase, useEffect, abort. Yana kerak bo'ladi: kesh, deduplikatsiya, fokusda yangilash, invalidatsiya... Bu 51-darsdagi muammoning aynan o'zi.

Shuning uchun Redux jamoasi RTK Query ni yaratdi.


RTK Query

Server ma'lumotlari uchun — TanStack Query ning Redux ichidagi muqobili:

// services/api.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';

export const api = createApi({
  reducerPath: 'api',
  baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
  tagTypes: ['Mahsulot'],
  endpoints: (builder) => ({
    getMahsulotlar: builder.query({
      query: (kategoriya) => `mahsulotlar?kategoriya=${kategoriya}`,
      providesTags: ['Mahsulot'],
    }),
    getMahsulot: builder.query({
      query: (id) => `mahsulotlar/${id}`,
      providesTags: (natija, xato, id) => [{ type: 'Mahsulot', id }],
    }),
    addMahsulot: builder.mutation({
      query: (yangi) => ({ url: 'mahsulotlar', method: 'POST', body: yangi }),
      invalidatesTags: ['Mahsulot'],
    }),
  }),
});

export const {
  useGetMahsulotlarQuery,
  useGetMahsulotQuery,
  useAddMahsulotMutation,
} = api;

Har bir endpoint uchun hook avtomatik yaratiladi: use + nom + Query/Mutation.

Store ga ulash

// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import { api } from '../services/api';

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

Komponentda

function Mahsulotlar({ kategoriya }) {
  const { data, isLoading, isFetching, error } = useGetMahsulotlarQuery(kategoriya);

  if (isLoading) return <Skeleton />;
  if (error) return <p role="alert">Xato yuz berdi</p>;

  return <ul>{data.map((m) => <li key={m.id}>{m.nomi}</li>)}</ul>;
}

function YangiMahsulot() {
  const [qosh, { isLoading }] = useAddMahsulotMutation();

  return (
    <button disabled={isLoading} onClick={() => qosh({ nomi: 'Kitob', narx: 50000 })}>
      Qo'shish
    </button>
  );
}

thunk, extraReducers, useEffect — hech biri yo'q. Kesh, deduplikatsiya, race condition — avtomatik.

Teglar — invalidatsiya

  • providesTags: ['Mahsulot'] — "bu so'rov Mahsulot ma'lumotini beradi"
  • invalidatesTags: ['Mahsulot'] — "bu mutatsiya Mahsulot ni eskirtiradi"

Mahsulot qo'shilganda Mahsulot tegli barcha so'rovlar avtomatik qayta yuklanadi — TanStack Query dagi invalidateQueries (53-dars) ning deklarativ ko'rinishi.


Qaysi birini tanlash

Vaziyat Tanlov
Server ma'lumotlarini olish va o'zgartirish RTK Query (yoki TanStack Query)
Bir nechta slice ga ta'sir qiluvchi murakkab async jarayon (masalan, login → profil yuklash → sozlamalarni qo'llash) createAsyncThunk
Loyihada Redux yo'q TanStack Query (51–55-darslar)
Loyihada Redux bor RTK Query — qo'shimcha kutubxonasiz

[!TIP] RTK Query va TanStack Query — bir xil muammoni hal qiladi. Bitta loyihada ikkalasini ishlatmang. Agar Redux allaqachon bor bo'lsa — RTK Query; Redux yo'q bo'lsa, faqat server ma'lumotlari uchun Redux qo'shishning ma'nosi yo'q — TanStack Query.


Nima uchun shunday?

Nima uchun Redux reducer larida async kod taqiqlangan — bu shunchaki noqulay cheklov emasmi?

Bu Redux ning eng muhim kafolati: bir xil state + bir xil action = har doim bir xil natija. Shu kafolat tufayli DevTools da "vaqt bo'ylab sayohat" mumkin — eski action larni qayta "o'ynatish" har doim aynan o'sha state ni beradi. Agar reducer ichida fetch bo'lsa, har bir qayta o'ynatish yangi so'rov yuboradi va boshqa natija oladi — tarix ma'nosiz bo'ladi. Thunk lar bu muammoni chiroyli ajratadi: "dunyo bilan aloqa" (tarmoq, vaqt, tasodifiylik) thunk da, "state qanday o'zgaradi" esa sof reducer da. Natijada tarixdagi har bir action — server javobi bilan birga — yozib qo'yilgan va qayta ishlab chiqarish mumkin bo'lgan haqiqat.


Xulosa

Eslab qoling:

  • Reducer — sof; async kod thunk da
  • createAsyncThunk(type, async (arg, { signal, rejectWithValue }) => ...) → pending, fulfilled, rejected
  • Natijalar extraReducers da builder.addCase(...) bilan
  • dispatch(thunk(arg)) → .abort() va .unwrap()
  • RTK Query: createApi + fetchBaseQuery + endpoints → avtomatik hook lar
  • providesTags / invalidatesTags — avtomatik qayta yuklash
  • Server ma'lumoti — RTK Query yoki TanStack Query (ikkalasi emas); murakkab jarayon — thunk

Keyingi bo'lim

9-bo'lim: Formalar — HTML forma, React Hook Form va Zod.