IlmHamroh
JavaScript Full-stack/17-qism. React asoslari42/45-dars19 daqiqa
Mundarija (26)

React Context II: useReducer bilan global holat, qayta chizish narxi va chegaralar

Qisqacha: Context + useReducer — kutubxonasiz kichik global holat: Provider komponent reducer'ni saqlaydi va holatni bir kontekstda, dispatch ni boshqasida beradi. Kontekstni to'g'ridan-to'g'ri emas, useCart() kabi custom hook orqali o'qing: Provider unutilsa, hook null o'rniga aniq xato beradi. Narxi: Provider qiymati o'zgarsa, uni o'qiyotgan hamma komponent qayta chiziladi. Holat va dispatch ni ajratish, children orqali chizish va kontekstlarni ma'no bo'yicha bo'lish bu narxni kamaytiradi. Tez-tez o'zgaradigan yoki katta ma'lumot uchun esa maxsus vositalar bor.

Bu darsda

  • useReducer va Context'ni birlashtirib, savatni butun ilovaga ulaysiz.
  • Holat va dispatch ni ikki kontekstga ajratib, keraksiz qayta chizishni yo'qotasiz.
  • useCart() kabi hook bilan Provider'siz ishlatishni aniq xatoga aylantirasiz.
  • Kontekst qiymati o'zgarganda kim qayta chizilishini konsolda ko'rsatib bera olasiz.
  • Context qayerda yetmasligini va o'rniga nima kelishini bilasiz.

Oldin bilishingiz kerak: Context I, useReducer, useSyncExternalStore, children va kompozitsiya.

1. Nega bu kerak?

«Bahor» saytida savat uch joyda kerak: tepadagi belgi ("Savatda: 2"), menyudagi «Qo'shish» tugmalari va alohida savat sahifasi. useReducer darsida savat reducer'ini yozdik, Context I da qiymatni pastga yetkazishni o'rgandik. Endi ikkalasini birlashtiramiz — natija kichik "global do'kon" bo'ladi, hech qanday kutubxonasiz.

Lekin Sardor birinchi variantni yozib, React DevTools'da "Highlight updates" ni yoqdi (React DevTools): har «Qo'shish» bosilganda menyudagi hamma tugmalar yonib-o'chdi. Ular savat tarkibini ko'rsatmaydi, faqat dispatch ni chaqiradi — nega ular qayta chiziladi?

Hayotiy o'xshatish: oshxonadagi e'lonlar taxtasi. Unga hamma narsa yozilsa (bugungi menyu, oylik, tozalash navbati), har o'zgarishda hamma xodim kelib taxtani qayta o'qiydi. Oylik o'zgargan bo'lsa ham oshpaz menyuni qayta tekshiradi. Yechim — har mavzuga alohida taxta. Kontekstlarni ham shunday bo'lamiz.

2. Context + useReducer

2.1 Provider komponenti

Naqsh uch qismdan iborat: reducer, Provider komponenti va iste'molchilar.

jsx
import { createContext, use, useReducer } from "react";

const CartContext = createContext(null);

function cartReducer(cart, action) {
  switch (action.type) {
    case "added":
      return [...cart, action.name];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, []);
  return (
    <CartContext value={{ cart, dispatch }}>{children}</CartContext>
  );
}

function Badge() {
  const { cart } = use(CartContext);
  console.log("Badge");
  return <p>Savatda: {cart.length}</p>;
}

function AddButton() {
  const { dispatch } = use(CartContext);
  console.log("AddButton");
  const add = () => dispatch({ type: "added", name: "Osh" });
  return <button onClick={add}>Osh +</button>;
}

function Menu() {
  console.log("Menu");
  return <section><AddButton /></section>;
}

function App() {
  return (
    <CartProvider>
      <Badge />
      <Menu />
    </CartProvider>
  );
}

Konsolda:

text
Badge
Menu
AddButton
Badge
AddButton
Badge
AddButton

Ekranda:

text
Savatda: 2
Osh +

CartProvider — oddiy komponent: ichida useReducer, natijasini kontekstga beradi, children ni chizadi. App uni ilovaning tepasiga qo'yadi va savat hamma joyda bor bo'ladi.

2.2 Kim qayta chizildi

Konsolni o'qiymiz. Birinchi uch qator — birinchi chizish. Keyin har bosishda ikki qator: Badge va AddButton.

Qadam cart Qayta chizildi Nega
birinchi chizish [] Badge, Menu, AddButton hammasi yangi
«Osh +» [Osh] Badge, AddButton ikkalasi CartContext ni o'qiydi
«Osh +» [Osh, Osh] Badge, AddButton —

Ikki narsaga e'tibor bering.

Menu qayta chizilmadi. U kontekstni o'qimaydi. U App da yaratilgan va CartProvider ga children sifatida berilgan. CartProvider qayta chizilganda children — o'sha elementlar. React ularni qayta chizmaydi, faqat kontekstni o'qiyotganlarini "uyg'otadi". Bu — children naqshining bepul foydasi (children va kompozitsiya).

AddButton keraksiz qayta chizildi. U faqat dispatch ni oladi, dispatch esa hech qachon o'zgarmaydi. Lekin kontekst qiymati — { cart, dispatch } obyekti — har renderda yangi. React Object.is(eski, yangi) qiladi, false oladi va kontekstni o'qigan hammani qayta chizadi. Kontekst "obyektning qaysi qismini ishlatding" deb so'ramaydi.

3. Holat va dispatch — ikki kontekstda

react.dev'dagi "Scaling Up with Reducer and Context" sahifasi aynan shuni tavsiya qiladi: holat bir kontekstda, dispatch boshqasida.

jsx
import { createContext, use, useReducer } from "react";

const CartContext = createContext(null);
const CartDispatchContext = createContext(null);

function cartReducer(cart, action) {
  switch (action.type) {
    case "added":
      return [...cart, action.name];
    default:
      throw new TypeError(`Noma'lum amal: ${action.type}`);
  }
}

function CartProvider({ children }) {
  const [cart, dispatch] = useReducer(cartReducer, []);
  return (
    <CartContext value={cart}>
      <CartDispatchContext value={dispatch}>
        {children}
      </CartDispatchContext>
    </CartContext>
  );
}

function Badge() {
  const cart = use(CartContext);
  console.log("Badge");
  return <p>Savatda: {cart.length}</p>;
}

function AddButton() {
  const dispatch = use(CartDispatchContext);
  console.log("AddButton");
  const add = () => dispatch({ type: "added", name: "Osh" });
  return <button onClick={add}>Osh +</button>;
}

function Menu() {
  console.log("Menu");
  return <section><AddButton /></section>;
}

function App() {
  return (
    <CartProvider>
      <Badge />
      <Menu />
    </CartProvider>
  );
}

Konsolda:

text
Badge
Menu
AddButton
Badge
Badge

Ekranda:

text
Savatda: 2
Osh +

Endi bosilganda faqat Badge qayta chiziladi. CartDispatchContext ning qiymati — dispatch ning o'zi. React uni renderlar orasida barqaror saqlaydi (useReducer kafolati), shuning uchun Object.is → true va AddButton tinch qoladi. CartContext qiymati esa cart massivi — u faqat savat o'zgarganda yangi.

Kontekst Qiymati Qachon o'zgaradi Kim o'qiydi
CartContext cart taom qo'shilsa/olinsa belgi, savat sahifasi
CartDispatchContext dispatch hech qachon hamma tugmalar

Menyuda 50 ta taom bo'lsa, oldingi variantda har bosishda 50 ta tugma qayta chizilardi, endi — 0 ta.

flowchart TD
  P["CartProvider: useReducer"] --> C1["CartContext = cart"]
  P --> C2["CartDispatchContext = dispatch"]
  C1 -->|"savat o'zgarsa"| B["Badge — qayta chiziladi"]
  C2 -->|"hech qachon o'zgarmaydi"| A["AddButton — tinch"]
  P --> M["Menu — children, kontekst o'qimaydi"]
  M --> A

Diagrammada ikki "kanal": savat kanali o'zgaradi va faqat o'z o'quvchisini uyg'otadi, dispatch kanali esa jim. Menu hech qaysi kanalga ulanmagan.

Tekshirib ko'ring: CartProvider da const value = { cart, dispatch } ni komponentdan tashqarida e'lon qilsak, muammo yo'qoladimi?

Javob

Bo'lmaydi: cart va dispatch faqat komponent ichida, useReducer dan keyin mavjud. Tashqarida ular yo'q. Obyektni renderlar orasida "bir xil" saqlash uchun React'da useMemo hook'i bor (memo, useMemo, useCallback, 18-qism). Lekin ikki kontekstga ajratish — memo'siz, oddiy va tushunarli yechim.

4. Custom hook + null tekshiruv

4.1 Muammo: jim null

Kontekstlarning default qiymati — null. Kimdir Badge ni CartProvider dan tashqarida ishlatib qo'ysa-chi? use(CartContext) null qaytaradi va xato boshqa joyda chiqadi: Cannot read properties of null (reading 'length'). Xabar Provider haqida hech narsa demaydi.

Yechim — kontekstni to'g'ridan-to'g'ri emas, kichik funksiya orqali o'qish. U null ni tekshirib, tushunarli xato tashlaydi:

jsx
import { createContext, use } from "react";

const CartContext = createContext(null);

function useCart() {
  const cart = use(CartContext);
  if (cart === null) {
    throw new Error("useCart() — <CartProvider> ichida");
  }
  return cart;
}

function Badge() {
  const cart = useCart();
  return <p>Savatda: {cart.length}</p>;
}

function App() {
  return <Badge />; // ❌ CartProvider unutildi
}

Konsolda:

text
Uncaught Error: useCart() — <CartProvider> ichida

Xato darhol, aniq joyda va aniq so'zlar bilan. useCart — bu custom hook: ichida boshqa hook (use) chaqiradigan, nomi use bilan boshlanadigan oddiy funksiya. Ularni keyingi darsda chuqur o'rganamiz. Hozir bitta foydasini ko'rdik: kontekst tafsilotlari (qaysi kontekst, null tekshiruvi) bir joyda yashirindi. Komponentlar faqat useCart() deydi.

4.2 Nega default null, "bo'sh savat" emas

Context I da default qiymatni "zaxira" sifatida ishlatgan edik: createContext("uz"). Til uchun bu to'g'ri — Provider'siz ham ma'noli qiymat bor. Savat yoki foydalanuvchi uchun esa "zaxira" yolg'on bo'ladi. createContext([]) bilan Provider'siz Badge "Savatda: 0" ko'rsatardi. dispatch esa () => {} bo'lib, tugmalar jim ishlamay qolardi. Dasturchi xatoni bir necha kundan keyin, mijozdan eshitardi.

Qoida: Provider'siz ma'nosi yo'q kontekstda default — null, o'qish — tekshiruvchi hook orqali. Shunday hooklarni ko'p uchratasiz: useAuth(), useTheme(), useRouter(). Hammasi bir xil tuzilgan. AuthContext ni 18-qismda xuddi shunday quramiz.

TypeScript'da bu naqsh turni ham toraytiradi: createContext<Cart | null>(null), hook esa Cart qaytaradi — iste'molchida null tekshirish shart emas.

5. Qayta chizish narxini kamaytirish

5.1 Qoidalar

Uch usulni ko'rdik, bittasini nomladik:

Usul Nima beradi
Provider'ni alohida komponent qilib, children olish Provider state'i o'zgarganda faqat iste'molchilar qayta chiziladi
holat va dispatch ni ajratish faqat amal yuboradiganlar qayta chizilmaydi
kontekstlarni ma'no bo'yicha bo'lish (tema, savat, foydalanuvchi) tema o'zgarsa savat iste'molchilari tinch
qiymat obyektini useMemo bilan saqlash Provider egasi boshqa sabab bilan qayta chizilsa ham qiymat "o'sha" (18-qism)

Oxirgi qator haqida qisqacha. Provider'ning egasi boshqa sabab bilan qayta chizilsa (masalan, App dagi soat), value={{ theme }} yangi obyekt bo'ladi va hamma iste'molchi qayta chiziladi. Hozircha bu sezilmaydi: React baribir otasi qayta chizilgan bolalarni ham qayta chizadi. Farq memo ishlatilganda ko'rinadi — bolani "props o'zgarmasa, qayta chizma" deb belgilasangiz, yangi kontekst obyekti bu himoyani buzib o'tadi. Shuning uchun memo bilan birga useMemo ham keladi — ikkalasi 18-qismda.

5.2 Qachon o'ylash kerak

Qayta chizish — o'z-o'zidan xato emas. React tez; kichik komponentni qayta chizish mikrosoniyalar. Mini-ilova darsida 1 000 vazifali ro'yxatda bitta katakni belgilash React'da 19–20 ms (vanilla'da 47–77 ms) olganini o'lchagan edik. Avval o'lchang: React DevTools Profiler (Performance'ni o'lchash), keyin tuzating. vazifalar kanonida tema kontekstida bitta kichik iste'molchi bor — kanon izohida yozilganidek, u yerda memo shart emas.

6. Context qachon yetmaydi

Context — yetkazish mexanizmi, holat boshqaruvchisi emas. U uch holatda noqulay:

  • Tez-tez o'zgaradigan qiymat. Sichqoncha joylashuvi, har harfda yoziladigan matn, taymer. Har o'zgarishda hamma iste'molchi qayta chiziladi.
  • Iste'molchiga qiymatning faqat bir qismi kerak. Kontekstda "selektor" yo'q: savatdan faqat count kerak bo'lsa ham, savat o'zgarsa qayta chizilasiz. useSyncExternalStore darsida o'zimiz yozgan useStore(store, selector) yoki Zustand aynan shuni beradi.
  • Server ma'lumoti. Menyu, buyurtmalar ro'yxati — ular serverda yashaydi va kesh, qayta yuklash, eskirish kabi muammolari bor. Buning uchun TanStack Query kabi vositalar (Server state vs client state).
Ma'lumot turi Qayerda yashaydi Vosita
bitta komponentniki komponent useState, useReducer
bir necha qo'shni komponentniki umumiy ota state'ni ko'tarish
butun ilova "muhiti" (tema, til, foydalanuvchi) Provider Context
tez-tez o'zgaradigan global holat, selektor kerak tashqi store Zustand, Redux, useSyncExternalStore
serverdagi ma'lumot kesh TanStack Query (19-qism)
ulashiladigan tanlov (filtr, sahifa) URL useSyncExternalStore, router

vazifalar bu jadvalga mos: filtr va saralash — URL'da (useSyncExternalStore), vazifalar va tahrir — App dagi reducer'da, qator hodisalari va tema — Context'da.

7. Vazifalar qadami: tema — Context orqali (2-qism) va commit

7.1 Tema qayerda edi

Context I darsida qator hodisalarini Context'ga o'tkazdik. Shu branch'da (refactor/kontekst) ikkinchi qism — tema. Holat 39 da tema tanlovi uch joyga bo'lingan edi:

  • TemaTanlov.tsx — o'z useState i va localStorage ga yozish;
  • main.tsx — ishga tushganda temaniQolla(saqlanganTanlov()) va tizimTemasiniKuzat() — matchMedia tinglovchisi modulda, React'dan tashqarida;
  • tema-tanlash.ts — yordamchilar.

Tema kerak bo'lgan ikkinchi komponent paydo bo'lsa (masalan, grafik ranglari), u tanlovni qayerdan oladi? TemaTanlov ning state'i ichkarida yopiq. Tanlov va uni o'zgartiruvchi funksiya — butun ilovaga kerak "muhit" qiymati, ya'ni Context uchun klassik nomzod.

7.2 TemaKonteksti.tsx

tsx
type TemaQiymati = {
  tanlov: Tema;
  tanla: (yangi: Tema) => void;
};

// null — Provider yo'q (dasturchi xatosi): useTema() aniq
// xato beradi
const TemaKonteksti = createContext<TemaQiymati | null>(null);

export function TemaProvider({ children }: { children: ReactNode }) {
  // Funksiya — localStorage faqat birinchi renderda o'qiladi
  const [tanlov, setTanlov] = useState(saqlanganTanlov);
  // Tizim qorong'imi — brauzer biladi (matchMedia): tashqi manba
  const tizimQorongi = useSyncExternalStore(
    tizimgaObuna,
    tizimQorongimi,
    () => false,
  );

  // Tanlov yoki tizim o'zgarsa — <html> va panel rangi ergashadi.
  // v6 da matchMedia tinglovchisi modulda qo'lda ulanardi (temaniUla)
  useEffect(() => {
    temaniQolla(tanlov, tizimQorongi);
  }, [tanlov, tizimQorongi]);

  function tanla(yangi: Tema) {
    setTanlov(yangi);
    temaniSaqla(yangi); // xotira — hodisada (keyingi ochilishga)
  }

  // React 19: <Context value> — .Provider yozish shart emas. Qiymat
  // har renderda yangi obyekt — iste'molchilar ham qayta chiziladi;
  // bizda bitta kichik iste'molchi, o'lchash va memo — 18-qism
  return (
    <TemaKonteksti value={{ tanlov, tanla }}>{children}</TemaKonteksti>
  );
}

/**
 * Tema konteksti. Provider ichida bo'lmasa — aniq xato (null emas)
 */
export function useTema(): TemaQiymati {
  // use() — useContext kabi, lekin shart ichida ham chaqirsa
  // bo'ladi
  const qiymat = use(TemaKonteksti);
  if (qiymat === null) {
    throw new Error("useTema() — <TemaProvider> ichida chaqirilsin");
  }
  return qiymat;
}

Fayl boshidagi importlar tushirib qoldirildi, bir nechta izoh qatori telefonga sig'ishi uchun qayta bo'lindi (matni o'sha). Bu bitta faylda butun 17-qism jamlangan:

  • useState(saqlanganTanlov) — lazy boshlang'ich qiymat (useState);
  • useSyncExternalStore — tizim temasi tashqi manba (useSyncExternalStore): tizimgaObuna matchMedia(...).addEventListener("change", …) ni ulaydi;
  • useEffect — <html data-tema> va telefon paneli rangi React'dan tashqaridagi DOM, effekt bilan sinxronlanadi (useEffect I);
  • localStorage ga yozish — effektda emas, hodisada (tanla): "foydalanuvchi tanladi" — bu hodisa (Sizga effekt kerak emas);
  • default null + useTema() — bugungi naqsh.

7.3 Iste'molchi va kirish nuqtasi

TemaTanlov.tsx endi state saqlamaydi:

diff
-  const [tanlov, setTanlov] = useState(saqlanganTanlov);
+  // Tanlov — kontekstdan (17/#42): state endi TemaProvider'da
+  const { tanlov, tanla } = useTema();

   function tanlandi(event: ChangeEvent<HTMLInputElement>) {
-    const yangi = tanlovniOl(event.target.value);
-    setTanlov(yangi);
-    tanlovniSaqla(yangi); // xotira, <html data-tema>, panel rangi
+    tanla(tanlovniOl(event.target.value));
   }

main.tsx — ilova Provider bilan o'raldi, qo'lda tinglovchi olib tashlandi:

diff
     <StrictMode>
-      <App
-        boshlangichRoyxat={yukla() ?? boshlangichVazifalar}
-        qoralama={qoralamaniOl()}
-      />
+      <TemaProvider>
+        <App
+          boshlangichRoyxat={yukla() ?? boshlangichVazifalar}
+          qoralama={qoralamaniOl()}
+        />
+      </TemaProvider>
     </StrictMode>,
 …
-// Panel rangi (theme-color) — saqlangan tanlov bo'yicha; <html
-// data-tema> ni tema.js allaqachon qo'ygan
-temaniQolla(saqlanganTanlov());
-tizimTemasiniKuzat();

7.4 Testlar

App.test.tsx dagi har render endi <TemaProvider> bilan o'ralgan — App ichidagi TemaTanlov Provider'siz xato berardi. jsdom'da matchMedia yo'q, shuning uchun vitest-sozlash.ts ga oddiy o'rinbosar qo'shildi: doim "yorug'" tizim, tinglovchilar hech narsa qilmaydi.

Yangi test — null tekshiruvining o'zi:

tsx
test("Context'siz qator — aniq xato (useQatorAmallari null tekshiruvi)", () => {
  const jim = vi.spyOn(console, "error").mockImplementation(() => {});
  expect(() =>
    render(
      <ul>
        <VazifaRoyxati
          vazifalar={vazifalar}
          sorov=""
          tahrirId={null}
        />
      </ul>,
    ),
  ).toThrow(
    "useQatorAmallari() — <QatorAmallariKonteksti value> ichida",
  );
  jim.mockRestore();
});

vi.spyOn(console, "error") — React yiqilgan render haqida konsolga yozadi, test chiqishi toza bo'lishi uchun vaqtincha "jim" qilinadi. Haqiqiy natija (npm run test):

text
ℹ tests 156
ℹ pass 156
ℹ fail 0
 Test Files  4 passed (4)
      Tests  25 passed (25)

node:test — 156, Vitest — 25 (+1). Brauzer tekshiruvi 159/159, jumladan tema bo'limi — masalan, tizim qorong'iga o'tganda panel rangi change hodisasi bilan yangilanadi (endi useSyncExternalStore orqali):

text
✅ tema: tizim qorong'i — panel (change hodisasi): "#14201a"
✅ tema: qorong'i saqlandi: ["rgb(20, 32, 26)","#14201a",true]

Diff (ikki darsdagi o'zgarishlar birga): 13 fayl, +239 −125. Commit:

text
refactor: tema va qator hodisalari Context orqali

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
faqat tugmalar ham qayta chiziladi value={{ state, dispatch }} — har renderda yangi obyekt holat va dispatch ni ikki kontekstga
Cannot read properties of null iste'molchida Provider unutilgan, default null useX() hook'ida null tekshiruv va aniq xato
Provider'siz "bo'sh" qiymat, tugmalar jim default [] yoki () => {} default null + tekshiruv
Provider ostidagi hamma narsa qayta chiziladi Provider ichida bolalar to'g'ridan-to'g'ri yozilgan Provider komponenti + children
har harfda sahifa sekinlashadi tez o'zgaradigan qiymat kontekstda lokal state yoki selektorli store
testda "Provider ichida chaqirilsin" test komponentni Provider'siz chizdi testda ham Provider bilan o'rang

9. Mashqlar

Mashqlar kurs/mashqlar/17/42-context-reducer/ da.

1-mashq (oson): himoyalangan hook

Bo'sh joylarni to'ldiring: Provider'siz chaqirilsa, hook aniq xato bersin.

jsx
const OrdersContext = createContext([___:null]);

function useOrders() {
  const orders = use(OrdersContext);
  if (orders [___:=== null]) {
    throw new Error("useOrders() — <OrdersProvider> ichida");
  }
  return orders;
}
Yechim
jsx
import { createContext, use } from "react";

const OrdersContext = createContext(null);

function useOrders() {
  const orders = use(OrdersContext);
  if (orders === null) {
    throw new Error("useOrders() — <OrdersProvider> ichida");
  }
  return orders;
}

function Counter() {
  return <p>Buyurtmalar: {useOrders().length}</p>;
}

function App() {
  return (
    <div>
      <OrdersContext value={["Osh", "Manti"]}>
        <Counter />
      </OrdersContext>
      <Counter />
    </div>
  );
}

Konsolda:

text
Uncaught Error: useOrders() — <OrdersProvider> ichida

Birinchi Counter ishlaydi, ikkinchisi (Provider'siz) — aniq xato. Haqiqiy ilovada Provider'ni ham alohida OrdersProvider komponentiga o'rash qoladi.

2-mashq (o'rta): kim qayta chiziladi?

Kodni ishga tushirmasdan, «Choy +» bir marta bosilganda konsolga qaysi komponentlar yozilishini ayting. Keyin Total qayta chizilishini saqlab, Counter va ChoyButton keraksiz qayta chizilmasin — kontekstni bo'ling.

jsx
import { createContext, use, useReducer } from "react";

const ShiftContext = createContext(null);

function reducer(state, action) {
  switch (action.type) {
    case "sold":
      return { ...state, total: state.total + action.price };
    default:
      return state;
  }
}

function ShiftProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, {
    cashier: "Sardor",
    total: 0,
  });
  const value = { state, dispatch }; // har renderda yangi obyekt
  return <ShiftContext value={value}>{children}</ShiftContext>;
}

function Counter() {
  const { state } = use(ShiftContext);
  console.log("Counter");
  return <p>Kassir: {state.cashier}</p>;
}

function Total() {
  const { state } = use(ShiftContext);
  console.log("Total");
  return <p>Tushum: {state.total}</p>;
}

function ChoyButton() {
  const { dispatch } = use(ShiftContext);
  console.log("ChoyButton");
  const sell = () => dispatch({ type: "sold", price: 5000 });
  return <button onClick={sell}>Choy +</button>;
}

function App() {
  return (
    <ShiftProvider>
      <Counter />
      <Total />
      <ChoyButton />
    </ShiftProvider>
  );
}

Bosilgandan keyin konsolga qo'shilgan qatorlar: Counter, [:Total], [:ChoyButton].

Yechim

Uchalasi ham — ular bitta kontekstni o'qiydi va qiymat ({ state, dispatch }) har safar yangi obyekt. Kassir ismi o'zgarmasa ham Counter qayta chizildi.

Tuzatish: kassir (smena davomida o'zgarmaydi), tushum va dispatch — uchta alohida kontekst.

jsx
import { createContext, use, useReducer } from "react";

const CashierContext = createContext(null);
const TotalContext = createContext(null);
const ShiftDispatchContext = createContext(null);

function reducer(state, action) {
  switch (action.type) {
    case "sold":
      return { ...state, total: state.total + action.price };
    default:
      return state;
  }
}

function ShiftProvider({ children }) {
  const [state, dispatch] = useReducer(reducer, {
    cashier: "Sardor",
    total: 0,
  });
  return (
    <CashierContext value={state.cashier}>
      <TotalContext value={state.total}>
        <ShiftDispatchContext value={dispatch}>
          {children}
        </ShiftDispatchContext>
      </TotalContext>
    </CashierContext>
  );
}

function Counter() {
  const cashier = use(CashierContext);
  console.log("Counter");
  return <p>Kassir: {cashier}</p>;
}

function Total() {
  const total = use(TotalContext);
  console.log("Total");
  return <p>Tushum: {total}</p>;
}

function ChoyButton() {
  const dispatch = use(ShiftDispatchContext);
  console.log("ChoyButton");
  const sell = () => dispatch({ type: "sold", price: 5000 });
  return <button onClick={sell}>Choy +</button>;
}

function App() {
  return (
    <ShiftProvider>
      <Counter />
      <Total />
      <ChoyButton />
    </ShiftProvider>
  );
}

Konsolda:

text
Counter
Total
ChoyButton
Total

Ekranda:

text
Kassir: Sardor
Tushum: 5000
Choy +

Kontekst qiymatlari — satr, son va dispatch: Object.is ularni qiymati bo'yicha solishtiradi. Kassir ismi o'sha satr — Counter tinch.

3-mashq (qiyin): AuthProvider

TypeScript'da kichik autentifikatsiya holatini yozing ("kim tizimga kirgan"):

  • holat — { status: "mehmon" } | { status: "kirgan"; name: string } (holat mashinasi tafakkuri);
  • amallar — loggedIn (name bilan) va loggedOut;
  • AuthProvider — useReducer + ikki kontekst (holat va dispatch);
  • useAuth() va useAuthDispatch() — null tekshiruvli hooklar;
  • Header — "Kirish" tugmasi yoki "Salom, Malika! · Chiqish"; Orders — faqat kirganlarga "Buyurtmalaringiz: 2 ta", mehmonga "Avval tizimga kiring".
Yechim
tsx
import { createContext, use, useReducer } from "react";
import type { Dispatch, ReactNode } from "react";

type Auth = { status: "mehmon" } | { status: "kirgan"; name: string };
type AuthAction =
  | { type: "loggedIn"; name: string }
  | { type: "loggedOut" };
type AuthDispatch = Dispatch<AuthAction>;

function authReducer(state: Auth, action: AuthAction): Auth {
  switch (action.type) {
    case "loggedIn":
      return { status: "kirgan", name: action.name };
    case "loggedOut":
      return state.status === "mehmon" ? state : { status: "mehmon" };
  }
}

const AuthContext = createContext<Auth | null>(null);
const AuthDispatchContext = createContext<AuthDispatch | null>(null);

export function AuthProvider({ children }: { children: ReactNode }) {
  const [auth, dispatch] = useReducer(authReducer, {
    status: "mehmon",
  });
  return (
    <AuthContext value={auth}>
      <AuthDispatchContext value={dispatch}>
        {children}
      </AuthDispatchContext>
    </AuthContext>
  );
}

export function useAuth(): Auth {
  const auth = use(AuthContext);
  if (auth === null) {
    throw new Error("useAuth() — <AuthProvider> ichida");
  }
  return auth;
}

export function useAuthDispatch(): AuthDispatch {
  const dispatch = use(AuthDispatchContext);
  if (dispatch === null) {
    throw new Error("useAuthDispatch() — <AuthProvider> ichida");
  }
  return dispatch;
}

function Header() {
  const auth = useAuth();
  const dispatch = useAuthDispatch();
  if (auth.status === "mehmon") {
    const login = () =>
      dispatch({ type: "loggedIn", name: "Malika" });
    return <button onClick={login}>Kirish</button>;
  }
  const logout = () => dispatch({ type: "loggedOut" });
  return (
    <p>
      Salom, {auth.name}! · <button onClick={logout}>Chiqish</button>
    </p>
  );
}

function Orders() {
  const auth = useAuth();
  return auth.status === "kirgan" ? (
    <p>Buyurtmalaringiz: 2 ta</p>
  ) : (
    <p>Avval tizimga kiring</p>
  );
}

export default function App() {
  return (
    <AuthProvider>
      <Header />
      <Orders />
    </AuthProvider>
  );
}

Ekranda:

text
Salom, Malika! · Chiqish
Buyurtmalaringiz: 2 ta

auth.name faqat status === "kirgan" tekshiruvidan keyin o'qiladi — TypeScript boshqacha yozishga ruxsat bermaydi. loggedOut mehmon holatida o'sha holatni qaytaradi — qayta chizish yo'q. Haqiqiy autentifikatsiya (token, server, barcha tablarda chiqish) — React'da auth holati darsida.

10. Real ishda

  • Kichik va o'rta ilovalarda Context + useReducer — kutubxonasiz global holatning eng keng tarqalgan usuli: savat, foydalanuvchi, sozlamalar.
  • Kutubxonalar shu naqshni ishlatadi: ThemeProvider + useTheme(), AuthProvider + useAuth(), QueryClientProvider + useQueryClient(). Har birida null tekshiruvli hook bor.
  • Performance muammolari kod ko'rigida ko'pincha "kontekst qiymati har renderda yangi obyekt" yoki "hamma narsa bitta kontekstda" deb belgilanadi.
  • Intervyu savollari: "Context bilan Redux/Zustand farqi?", "Kontekst o'zgarsa kim qayta chiziladi?", "Nega state va dispatch ni alohida kontekstga qo'yamiz?", "Custom hook'da null tekshiruv nima beradi?".

Xulosa

  • Context + useReducer = kutubxonasiz global holat: Provider komponenti reducer'ni saqlaydi, children ni chizadi.
  • Kontekst qiymati o'zgarsa, uni o'qiyotgan hamma qayta chiziladi; { state, dispatch } obyekti har renderda yangi.
  • Holat va dispatch ni ajrating; kontekstlarni ma'no bo'yicha bo'ling; children naqshi oraliq komponentlarni himoya qiladi.
  • Default null + useX() hook'i null'ni aniq xatoga aylantiradi va turni toraytiradi.
  • Tez o'zgaradigan, selektor kerak yoki serverdagi ma'lumot — Context emas, maxsus vositalar.
  • vazifalar da tema TemaProvider + useTema() da: useState, useSyncExternalStore, effekt va hodisa bir faylda; commit — qator hodisalari bilan birga.

Keyingi dars: Custom hooklar I: mantiqni ajratib olish — useCart kabi o'z hooklarimizni qanday yozish, nomlash va nima qaytarishni o'rganamiz va App.tsx ni yengillatamiz.

Manbalar

  • React: "Scaling Up with Reducer and Context" — react.dev/learn/scaling-up-with-reducer-and-context
  • React: useContext — "Optimizing re-renders when passing objects and functions" — react.dev/reference/react/useContext
  • React: useReducer — dispatch barqarorligi — react.dev/reference/react/useReducer
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Context II: useReducer bilan global holat, qayta chizish narxi va chegaralar — IlmHamroh