React bilan real loyiha — API layer, hooks layer, UI layer

~25 daq · React · Ekotizim va Amaliyot

Muammodan boshlaylik

Odatiy komponent — hamma narsa bitta joyda:

function BuyurtmalarSahifasi() {
  const [buyurtmalar, setBuyurtmalar] = useState([]);
  const [yuklanmoqda, setYuklanmoqda] = useState(true);
  const [holat, setHolat] = useState('hammasi');

  useEffect(() => {
    const token = localStorage.getItem('token');
    fetch(`https://api.dokon.uz/v1/orders?status=${holat}`, {
      headers: { Authorization: `Bearer ${token}` },
    })
      .then((j) => j.json())
      .then((d) => {
        setBuyurtmalar(d.data.map((b) => ({ ...b, sana: new Date(b.created_at) })));
        setYuklanmoqda(false);
      });
  }, [holat]);

  async function bekorQil(id) {
    await fetch(`https://api.dokon.uz/v1/orders/${id}/cancel`, { method: 'POST', ... });
    setBuyurtmalar((b) => b.map((x) => (x.id === id ? { ...x, status: 'cancelled' } : x)));
  }

  return ( /* 120 qator JSX */ );
}

Bitta komponentda: URL lar, token, server formati (created_at, data.data), kesh, holatlar, biznes mantiq va UI. Natijada:

  • Backend created_at ni createdAt ga o'zgartirsa — barcha komponentlarni qidirib tuzatish kerak
  • Shu buyurtmalar ro'yxati boshqa sahifada ham kerak — kodni nusxalash
  • Testlash — butun komponentni server bilan birga sinash kerak

Yechim — mas'uliyatni qatlamlarga ajratish.


Uch qatlam

┌──────────────────────────────────────────────────┐
│ UI qatlami          — komponentlar               │  Qanday KO'RINADI
│   BuyurtmalarSahifasi, BuyurtmaKartochka         │
├──────────────────────────────────────────────────┤
│ Hooks qatlami       — TanStack Query, mantiq     │  QACHON va QANDAY olinadi,
│   useBuyurtmalar, useBuyurtmaBekorQil            │  kesh, holatlar
├──────────────────────────────────────────────────┤
│ API qatlami         — HTTP so'rovlar             │  QAYERDAN va QANDAY formatda
│   buyurtmalarApi.getAll(), .cancel(id)           │
├──────────────────────────────────────────────────┤
│ HTTP client         — Axios instance             │  Token, xatolar (85-dars)
└──────────────────────────────────────────────────┘

Har bir qatlam faqat pastdagi qatlam bilan gaplashadi va o'zidan pastdagining tafsilotlarini bilmaydi:

  • UI fetch ni, URL ni, token ni bilmaydi
  • Hook lar URL va server formatini bilmaydi
  • API qatlami React ni bilmaydi

1. API qatlami

// features/buyurtmalar/api.js
import { api } from '@/shared/api/client';           // Axios instance (85-dars)

// Server formati → ilova formati
function buyurtmaniMoslash(b) {
  return {
    id: b.id,
    raqam: b.order_number,
    holat: b.status,
    summa: b.total_amount,
    sana: new Date(b.created_at),
    mahsulotlar: b.items.map((i) => ({ id: i.product_id, nomi: i.name, soni: i.qty })),
  };
}

export const buyurtmalarApi = {
  async getAll({ holat, sahifa = 1 } = {}) {
    const { data } = await api.get('/orders', {
      params: { status: holat === 'hammasi' ? undefined : holat, page: sahifa },
    });
    return {
      buyurtmalar: data.data.map(buyurtmaniMoslash),
      jamiSahifalar: data.meta.last_page,
    };
  },

  async getById(id) {
    const { data } = await api.get(`/orders/${id}`);
    return buyurtmaniMoslash(data.data);
  },

  async cancel(id) {
    const { data } = await api.post(`/orders/${id}/cancel`);
    return buyurtmaniMoslash(data.data);
  },
};

API qatlami:

  • Faqat sof funksiyalar — React yo'q, hook yo'q
  • URL lar, parametrlar va HTTP metodlar — faqat shu yerda
  • Moslash (mapping): server formati (created_at, total_amount, data.data) → ilovaning qulay formati (sana: Date, summa)

Backend created_at ni createdAt ga o'zgartirsa — bitta funksiyani tuzatasiz. Ilovaning qolgan qismi hech narsani sezmaydi.

[!TIP] TypeScript da moslash funksiyasi yanada foydali: server javobi uchun bitta tur (ApiBuyurtma), ilova uchun boshqa (Buyurtma). Zod sxemasi (65-dars) bilan server javobini runtime da tekshirish ham mumkin: BuyurtmaSchema.parse(data) — backend kutilmagan format qaytarsa, xato aniq joyda va aniq xabar bilan chiqadi.


2. Hooks qatlami

Query key factory

// features/buyurtmalar/keys.js
export const buyurtmaKeys = {
  all: ['buyurtmalar'],
  lists: () => [...buyurtmaKeys.all, 'list'],
  list: (filtr) => [...buyurtmaKeys.lists(), filtr],
  details: () => [...buyurtmaKeys.all, 'detail'],
  detail: (id) => [...buyurtmaKeys.details(), id],
};

Barcha kalitlar bitta joyda — invalidatsiyada xato qilish qiyin (52–53-darslar):

queryClient.invalidateQueries({ queryKey: buyurtmaKeys.lists() });   // barcha ro'yxatlar
queryClient.invalidateQueries({ queryKey: buyurtmaKeys.all });       // hammasi

Hook lar

// features/buyurtmalar/hooks.js
import { useQuery, useMutation, useQueryClient, keepPreviousData } from '@tanstack/react-query';
import { buyurtmalarApi } from './api';
import { buyurtmaKeys } from './keys';

export function useBuyurtmalar(filtr) {
  return useQuery({
    queryKey: buyurtmaKeys.list(filtr),
    queryFn: () => buyurtmalarApi.getAll(filtr),
    placeholderData: keepPreviousData,
    staleTime: 30_000,
  });
}

export function useBuyurtma(id) {
  return useQuery({
    queryKey: buyurtmaKeys.detail(id),
    queryFn: () => buyurtmalarApi.getById(id),
    enabled: !!id,
  });
}

export function useBuyurtmaBekorQil() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: buyurtmalarApi.cancel,
    onSuccess: (yangilangan) => {
      queryClient.setQueryData(buyurtmaKeys.detail(yangilangan.id), yangilangan);
      return queryClient.invalidateQueries({ queryKey: buyurtmaKeys.lists() });
    },
  });
}

Hooks qatlami:

  • Qachon yuklash, qancha keshlash, nimani yangilash
  • Biznes qoidalari: "bekor qilingandan keyin ro'yxatni yangilash"
  • Komponentlar uchun oddiy interfeys: useBuyurtmalar(filtr)

Kesh strategiyasini o'zgartirish — bitta faylda, komponentlarga tegmasdan.


3. UI qatlami

// pages/BuyurtmalarSahifasi.jsx
import { useSearchParams } from 'react-router-dom';
import { useBuyurtmalar, BuyurtmaKartochka, HolatFiltri } from '@/features/buyurtmalar';

export default function BuyurtmalarSahifasi() {
  const [params, setParams] = useSearchParams();
  const holat = params.get('holat') ?? 'hammasi';
  const sahifa = Number(params.get('sahifa') ?? 1);

  const { data, isPending, isError, error, isPlaceholderData } = useBuyurtmalar({ holat, sahifa });

  return (
    <main>
      <h1>Buyurtmalarim</h1>
      <HolatFiltri qiymat={holat} onChange={(h) => setParams({ holat: h })} />

      {isPending ? (
        <BuyurtmalarSkeleton />
      ) : isError ? (
        <XatoBloki xabar={error.message} />
      ) : data.buyurtmalar.length === 0 ? (
        <p>Hali buyurtmalar yo'q</p>
      ) : (
        <ul style={{ opacity: isPlaceholderData ? 0.6 : 1 }}>
          {data.buyurtmalar.map((b) => (
            <li key={b.id}><BuyurtmaKartochka buyurtma={b} /></li>
          ))}
        </ul>
      )}
    </main>
  );
}
// features/buyurtmalar/components/BuyurtmaKartochka.jsx
import { formatNarx } from '@/shared/lib/formatNarx';
import { useBuyurtmaBekorQil } from '../hooks';

export function BuyurtmaKartochka({ buyurtma }) {
  const bekorQil = useBuyurtmaBekorQil();

  return (
    <article>
      <h3>Buyurtma #{buyurtma.raqam}</h3>
      <p>{buyurtma.sana.toLocaleDateString('uz')} · {formatNarx(buyurtma.summa)}</p>
      <HolatBelgisi holat={buyurtma.holat} />
      {buyurtma.holat === 'pending' && (
        <button onClick={() => bekorQil.mutate(buyurtma.id)} disabled={bekorQil.isPending}>
          {bekorQil.isPending ? 'Bekor qilinmoqda...' : 'Bekor qilish'}
        </button>
      )}
    </article>
  );
}

UI qatlami faqat ko'rinish haqida o'ylaydi: holatlar, tartib, foydalanuvchi harakatlari. Unda URL ham, token ham, created_at ham, invalidateQueries ham yo'q.

Ko'rinish komponentlari

UI ichida ham ikki tur bor:

  • Ma'lumot bilan ishlovchi — hook larni chaqiradi (BuyurtmalarSahifasi)
  • Faqat ko'rinish — faqat props oladi (HolatBelgisi, formatNarx, shared/ui/Button)

Ko'rinish komponentlarini testlash va qayta ishlatish oson — ular hech narsaga bog'liq emas.


Natija — papka tuzilmasi

src/
├── shared/
│   ├── api/client.js            // Axios instance + interceptor lar
│   ├── ui/                      // Button, Skeleton, XatoBloki
│   └── lib/formatNarx.js
├── features/
│   └── buyurtmalar/
│       ├── api.js               // API qatlami + moslash
│       ├── keys.js              // query key factory
│       ├── hooks.js             // hooks qatlami
│       ├── components/
│       │   ├── BuyurtmaKartochka.jsx
│       │   └── HolatFiltri.jsx
│       └── index.js             // ochiq API
└── pages/
    └── BuyurtmalarSahifasi.jsx  // UI — sahifa

Har bir qatlamni testlash

Qatlam Qanday testlanadi
API MSW (78-dars) bilan: to'g'ri URL, to'g'ri moslash
Moslash funksiyasi Oddiy unit test (74-dars) — eng oson va eng foydali
Hooks renderHook + MSW (77-dars)
UI RTL + MSW (75–76-darslar): foydalanuvchi ssenariylari

[!WARNING] Qatlamlar — vosita, maqsad emas. Bitta oddiy so'rovli kichik sahifa uchun uch qatlam va key factory — ortiqcha murakkablik. Bu tuzilma loyiha o'sganda, bir xil ma'lumot ko'p joyda ishlatilganda va backend formati o'zgarishi mumkin bo'lganda o'zini oqlaydi.


Nima uchun shunday?

Nima uchun qatlamlarga ajratish vaqt o'tishi bilan bunchalik katta foyda beradi?

Chunki har bir qatlam turli sabablarga ko'ra o'zgaradi. API qatlami — backend o'zgarganda (yangi URL, boshqa format). Hooks qatlami — mahsulot talablari o'zgarganda (ma'lumot qanchalik yangi bo'lishi kerak, nimani yangilash kerak). UI — dizayn o'zgarganda. Hamma narsa bitta komponentda bo'lsa, har qanday o'zgarish hamma narsaga tegadi va xavf tug'diradi. Qatlamlarga ajratilganda esa o'zgarish o'z qatlamida qoladi: dizayner yangi kartochka chizdi — faqat UI; backend versiyasi yangilandi — faqat moslash funksiyasi. Bu dasturlashdagi "mas'uliyatlarni ajratish" (separation of concerns) tamoyili — va u katta loyihalarni boshqariladigan qiladi.


Xulosa

Eslab qoling:

  • Uch qatlam: API (qayerdan, qanday format) → Hooks (qachon, kesh, biznes qoidalar) → UI (qanday ko'rinadi)
  • API qatlami: sof funksiyalar, URL lar faqat shu yerda, server formati → ilova formatiga moslash
  • Hooks qatlami: TanStack Query, query key factory, mutatsiyalar va invalidatsiya
  • UI qatlami: faqat holatlar va ko'rinish; URL, token, server formati yo'q
  • Ko'rinish komponentlari — faqat props; ma'lumot bilan ishlovchilar — hook larni chaqiradi
  • Har bir qatlam alohida testlanadi; moslash funksiyasi — eng oson va foydali test
  • Kichik loyihalarda ortiqcha murakkablikdan saqlaning

Keyingi dars

90-dars: Deployment — Vercel, Netlify ga deploy qilish