IlmHamroh
JavaScript Full-stack/17-qism. React asoslari28/45-dars23 daqiqa
Mundarija (28)

useEffect III: cleanup, race condition va AbortController — effekt boshlagan ishni tozalash

Qisqacha: Effekt funksiyasi cleanup (tozalash) funksiyasini qaytarishi mumkin: useEffect(() => { const id = setInterval(…); return () => clearInterval(id); }, []). React uni ikki paytda chaqiradi: effekt yangi qiymatlar bilan qayta ishlashidan oldin va komponent ekrandan olib tashlanganda. Qoida: effekt nima boshlasa (taymer, tinglovchi, obuna, so'rov) — cleanup o'shani to'xtatadi. Cleanup bo'lmasa, tinglovchilar ko'payib boradi va kech kelgan server javobi yangi javobning ustiga yoziladi (race condition). So'rov uchun eng yaxshi cleanup — AbortController: return () => controller.abort().

Bu darsda

  • Cleanup funksiyasini yozasiz va u qachon chaqirilishini konsolda kuzatasiz.
  • setInterval va addEventListener ni effektda to'g'ri tozalaysiz.
  • Race condition'ni jonli ko'rasiz: tez javob sekin javobdan oldin keladi va ekran yolg'on gapiradi.
  • Uni ikki usulda tuzatasiz: ignore bayrog'i va AbortController.
  • vazifalar dagi tashqi tizimlarni (boshqa tablar, server, internet, URL) cleanup bilan effektlarga ko'chirasiz.

Oldin bilishingiz kerak: useEffect II, Taymerlar: setTimeout va setInterval, Bekor qilish: AbortController, fetch asoslari.

1. Nega bu kerak?

Oldingi darsda massiv effektning qachon qayta ishlashini belgilashini ko'rdik. Lekin effekt qayta ishlaganda, oldingisi boshlagan ish nima bo'ladi?

Sardor «Bahor» menyusiga ikki narsa qo'shdi. Birinchisi — oshxona soati: har soniyada vaqt yangilanadi. Ikkinchisi — kategoriya tugmalari: «Taom» yoki «Ichimlik» bosilsa, serverdan o'sha bo'lim yuklanadi. Uchinchi kuni Jasur aka ikki shikoyat bilan keldi. Birinchisi: "Telefonim qiziyapti, sahifa sekinlashdi". Ikkinchisi: "Men «Ichimlik» ni bosdim, tepada «Tanlangan: ichimlik» deb turibdi, ro'yxatda esa osh va manti!"

Ikkala muammoning sababi bitta: effekt ishni boshladi, lekin hech kim uni to'xtatmadi. Yopilgan soatning taymeri ishlashda davom etdi. Eski kategoriya uchun yuborilgan so'rov esa yangisidan kechroq qaytib, ekranni bosib ketdi.

Hayotiy o'xshatish: mehmonxona xonasi. Mehmon kirganda chiroq, konditsioner va televizorni yoqadi (effekt). Chiqib ketayotganda — o'chiradi (cleanup). Keyingi mehmon toza xonaga kiradi. Agar hech kim o'chirmasa, har yangi mehmon bilan yoqilgan asboblar soni ko'payib boradi va hisob kelganda hamma hayron bo'ladi.

2. Cleanup funksiyasi

2.1 Taymerni to'xtatish

Effekt funksiyasi funksiya qaytarsa, React uni eslab qoladi — bu cleanup (tozalash) funksiyasi. Uni React o'zi, kerakli paytda chaqiradi. Soat:

jsx
import { useEffect, useState } from "react";

function Clock() {
  const [ticks, setTicks] = useState(0);
  useEffect(() => {
    console.log("taymer boshlandi");
    const id = setInterval(() => {
      console.log("tik");
      setTicks((t) => t + 1);
    }, 250);
    return () => {
      clearInterval(id); // effekt boshlagan ishni to'xtatamiz
      console.log("taymer to'xtadi");
    };
  }, []);
  return <p>Tik: {ticks}</p>;
}

function App() {
  const [open, setOpen] = useState(true);
  return (
    <div>
      {open && <Clock />}
      <button onClick={() => setOpen(false)}>Yopish</button>
    </div>
  );
}

Konsolda:

text
taymer boshlandi
tik
tik
taymer to'xtadi

Ekranda:

text
Yopish

Blok 600 ms kutadi (ikki "tik"), keyin «Yopish» ni bosadi. Clock ekrandan olib tashlandi — React cleanup'ni chaqirdi, taymer to'xtadi. Shundan keyin konsolda boshqa "tik" yo'q.

Ikkita mayda, lekin muhim detal:

  • setTicks((t) => t + 1) — updater (State II). Taymer ichidagi funksiya bir marta yaratilgan va ticks ning birinchi qiymatini (0) ko'radi. setTicks(ticks + 1) yozsak, soat 1 da qotib qolardi. Updater esa har safar eng so'nggi qiymatni oladi. Shuning uchun massiv [] — halol: effekt ticks ni o'qimaydi.
  • id — effekt ichidagi o'zgaruvchi. Cleanup funksiyasi closure orqali aynan shu effektning id sini ko'radi. Har effektning o'z id si, o'z cleanup'i bor.

2.2 Cleanup bo'lmasa — tinglovchilar ko'payadi

Endi xatoni ko'ramiz. Taymer bilan ham ko'rsatsa bo'lardi, lekin vaqtga bog'liq natija har kompyuterda boshqacha. Shuning uchun window ga bosish tinglovchisini olamiz — natija aniq. Effekt category ga bog'liq va cleanup qaytarmaydi:

jsx
import { useEffect, useState } from "react";

function App() {
  const [category, setCategory] = useState("taom");
  useEffect(() => {
    function handleClick() {
      console.log("bosildi, kategoriya:", category);
    }
    window.addEventListener("click", handleClick); // ❌ cleanup yo'q
  }, [category]);
  return (
    <div>
      <button onClick={() => setCategory("taom")}>Taom</button>{" "}
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>{" "}
      <button>Hisob</button>
    </div>
  );
}

Konsolda:

text
bosildi, kategoriya: taom
bosildi, kategoriya: taom
bosildi, kategoriya: ichimlik
bosildi, kategoriya: taom
bosildi, kategoriya: ichimlik
bosildi, kategoriya: taom

Uch bosish — olti qator. Birinchi bosishda bitta tinglovchi bor edi, ikkinchisida — ikkita, uchinchisida — uchta. Har category o'zgarishi yangi tinglovchi qo'shdi, eskisini hech kim olib tashlamadi. Yana yomoni: eski tinglovchilar eski category ni ko'radi (har biri o'z closure'ida). Bir soatdan keyin yuzlab tinglovchi bo'ladi — telefon shu sababli qiziydi.

Tuzatish — bitta qator:

jsx
import { useEffect, useState } from "react";

function App() {
  const [category, setCategory] = useState("taom");
  useEffect(() => {
    function handleClick() {
      console.log("bosildi, kategoriya:", category);
    }
    window.addEventListener("click", handleClick);
    return () => window.removeEventListener("click", handleClick);
  }, [category]);
  return (
    <div>
      <button onClick={() => setCategory("taom")}>Taom</button>{" "}
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>{" "}
      <button>Hisob</button>
    </div>
  );
}

Konsolda:

text
bosildi, kategoriya: taom
bosildi, kategoriya: ichimlik
bosildi, kategoriya: taom

Har bosishda bitta qator, har doim joriy kategoriya. removeEventListener ga aynan o'sha funksiya beriladi (addEventListener asoslari darsidagi qoida). handleClick effekt ichida e'lon qilingani uchun cleanup uni closure orqali biladi.

Tekshirib ko'ring: Konsolning birinchi qatorida nega taom turibdi? Axir birinchi bosilgan tugma «Ichimlik» edi.

Javob

Bosish hodisasi avval tugmaning onClick ini chaqiradi (setCategory("ichimlik")), keyin window gacha ko'tariladi. Shu paytda window da hali eski tinglovchi turibdi — u chizishdan oldingi category ni (taom) ko'radi. Qayta chizish va effekt almashinuvi hodisa tugagandan keyin bo'ladi. Ikkinchi qatordan boshlab tinglovchi yangi.

3. Cleanup qachon chaqiriladi

3.1 Tartib

Cleanup ikki paytda ishlaydi:

  1. Effekt qayta ishlashidan oldin — massivdagi qiymat o'zgardi: avval eski effektning cleanup'i, keyin yangi effekt.
  2. Komponent ekrandan olib tashlanganda — oxirgi effektning cleanup'i.
jsx
import { useEffect, useState } from "react";

function Menu({ category }) {
  useEffect(() => {
    console.log("effekt (setup):", category);
    return () => console.log("cleanup:", category);
  }, [category]);
  console.log("render:", category);
  return <p>Menyu: {category}</p>;
}

function App() {
  const [category, setCategory] = useState("taom");
  const [open, setOpen] = useState(true);
  return (
    <div>
      {open && <Menu category={category} />}
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>{" "}
      <button onClick={() => setOpen(false)}>Yopish</button>
    </div>
  );
}

Konsolda:

text
render: taom
effekt (setup): taom
render: ichimlik
cleanup: taom
effekt (setup): ichimlik
cleanup: ichimlik

Har qatorni bosqichlarga joylaymiz:

Render Commit Cleanup Effekt
ochilish: taom DOM yaratildi — setup taom
«Ichimlik»: ichimlik matn yangilandi taom setup ichimlik
«Yopish»: App (Menu'siz) <p> olib tashlandi ichimlik —

Ikkinchi qatorga qarang: cleanup yangi chizishdan keyin, lekin yangi effektdan oldin ishladi. U eski qiymatni (taom) ko'radi — chunki u eski effektning closure'i. Effekt va uning cleanup'i — juftlik: bitta chizishdan, bir xil qiymatlar bilan.

sequenceDiagram
  participant R as React
  participant E1 as Effekt (taom)
  participant E2 as Effekt (ichimlik)
  R->>E1: setup — ulanish taom
  Note over R: category o'zgardi, qayta chizish
  R->>E1: cleanup — uzish taom
  R->>E2: setup — ulanish ichimlik
  Note over R: Menu yopildi
  R->>E2: cleanup — uzish ichimlik

Diagrammada har effektning hayoti ko'rinadi: u "boshlanadi" va "to'xtaydi", keyingisi boshlanishidan oldin. Bu "boshla — to'xtat" juftligi keyingi darsning asosiy g'oyasi.

3.2 Nima tozalanadi

Qoida oddiy: effekt boshlagan har narsa — cleanup'da to'xtatiladi. Effekt nimani boshlasa, xuddi o'sha narsa to'xtatiladi:

Effektda Cleanup'da
setInterval / setTimeout clearInterval / clearTimeout
addEventListener(tur, fn) removeEventListener(tur, fn)
kanal.addEventListener("message", fn) kanal.removeEventListener(…) yoki kanal.close()
new WebSocket(url) socket.close()
fetch(url, { signal }) controller.abort()
tashqi kutubxona: map = new Map(el) map.destroy() (kutubxonada qanday atalsa)

document.title va localStorage ga yozish — cleanup talab qilmaydi: ular "boshlanib, davom etadigan" ish emas. Keyingi effekt ularni shunchaki ustidan yozadi.

Tekshirib ko'ring: useEffect(() => { setTimeout(() => console.log("salom"), 3000); }, []) — komponent 1 soniyadan keyin yopilsa, konsolda "salom" chiqadimi? Qanday tuzatasiz?

Javob

Chiqadi: taymer brauzerda ishlayveradi, React u haqda hech narsa bilmaydi. Tuzatish: const id = setTimeout(…); return () => clearTimeout(id);. Taymer ichida setState bo'lsa, bu yanada muhim — yopilgan komponent uchun yangilanish keraksiz ish.

4. Race condition: kech kelgan javob

4.1 Jonli muammo

Jasur akaning ikkinchi shikoyati. Serverdan menyu olish — vaqt talab qiladi, javob tezligi esa har xil. Kurs serveri ?kechikish= parametri bilan javobni ataylab sekinlashtiradi. Biz shu bilan "taom sekin (1,5 s), ichimlik tez (0,3 s)" holatini yasadik. Kod — cleanup'siz:

jsx
import { useEffect, useState } from "react";

const API = "https://ilmhamroh.uz/api/mashq/menyu";

function App() {
  const [category, setCategory] = useState("taom");
  const [dishes, setDishes] = useState([]);
  useEffect(() => {
    // taom sekin (1500 ms), ichimlik tez (300 ms) — ataylab
    const delay = category === "taom" ? 1500 : 300;
    const url = `${API}?turkum=${category}&kechikish=${delay}`;
    console.log("so'rov:", category);
    fetch(url)
      .then((res) => res.json())
      .then((list) => {
        console.log("javob:", category, list.length, "ta");
        setDishes(list);
      });
  }, [category]);
  return (
    <div>
      <p>Tanlangan: {category}</p>
      <ul>{dishes.map((d) => <li key={d.id}>{d.nom}</li>)}</ul>
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>
    </div>
  );
}

«Ishga tushir» ni bosing va 1,5 soniya ichida «Ichimlik» ni bosing. Biz buni Chrome 154 da avtomatik qildik (ochilgandan 300 ms keyin bosish):

text
so'rov: taom
so'rov: ichimlik
javob: ichimlik 1 ta
javob: taom 3 ta

Ekranda: Tanlangan: ichimlik
Osh
Lag'mon
Manti

Tanlangan — ichimlik, ro'yxatda — taomlar. Vaqt chizig'ida nima bo'lgani:

Vaqt Voqea dishes
0 ms so'rov taom (1 500 ms) []
300 ms «Ichimlik» → so'rov ichimlik (300 ms) []
~600 ms javob ichimlik keldi → setDishes Ko'k choy
~1 500 ms javob taom keldi → setDishes Osh, Lag'mon, Manti

Race condition (poyga holati) — natija ishlar qaysi tartibda tugashiga bog'liq bo'lib qoladigan xato (fetch bilan race condition — 11-qismdan tanish). Ikki so'rov "poygaga chiqdi" va kechroq yuborilgani birinchi keldi. Tarmoqda bu oddiy hol: Wi-Fi, mobil internet, server yuklamasi.

Bu xatoning yomon tomoni — u sizning kompyuteringizda ko'pincha ko'rinmaydi. Lokal serverda hamma javob bir xil tez. Uni mehmon Chilonzordagi sekin 3G'da topadi.

4.2 Birinchi davo: ignore bayrog'i

Cleanup "bu effekt eskirdi" degan belgi qo'yishi mumkin. Effekt ichida let ignore = false, cleanup'da — true. Javob kelganda bayroqni tekshiramiz. Endi jonli blok vaqtga bog'liq bo'lmasligi uchun haqiqiy fetch o'rniga uning kichik nusxasini yozamiz: fetchMenu — xuddi shu kechikishlar bilan javob beradigan Promise (Promise asoslari).

jsx
import { useEffect, useState } from "react";

const MENU = [
  { id: 1, nom: "Osh", turkum: "taom" },
  { id: 2, nom: "Lag'mon", turkum: "taom" },
  { id: 4, nom: "Ko'k choy", turkum: "ichimlik" },
];

// fetch o'rnida: taom — 600 ms, ichimlik — 200 ms
function fetchMenu(category) {
  const delay = category === "taom" ? 600 : 200;
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(MENU.filter((d) => d.turkum === category));
    }, delay);
  });
}

function App() {
  const [category, setCategory] = useState("taom");
  const [dishes, setDishes] = useState([]);
  useEffect(() => {
    let ignore = false; // bu effekt eskirdimi?
    console.log("so'rov:", category);
    fetchMenu(category).then((list) => {
      if (ignore) {
        console.log("e'tiborsiz:", category);
        return;
      }
      console.log("javob:", category);
      setDishes(list);
    });
    return () => {
      ignore = true;
    };
  }, [category]);
  return (
    <div>
      <p>Tanlangan: {category}</p>
      <ul>{dishes.map((d) => <li key={d.id}>{d.nom}</li>)}</ul>
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>
    </div>
  );
}

Konsolda:

text
so'rov: taom
so'rov: ichimlik
javob: ichimlik
e'tiborsiz: taom

Ekranda:

text
Tanlangan: ichimlik
Ko'k choy
Ichimlik

taom javobi keldi, lekin uning effekti allaqachon "eskirgan" — ignore cleanup'da true bo'lgan. Har effektning o'z ignore o'zgaruvchisi bor (closure), shuning uchun yangi effektning bayrog'iga tegilmaydi. React hujjatidagi rasmiy naqsh aynan shu.

4.3 Ikkinchi davo: AbortController

ignore javobni e'tiborsiz qoldiradi, lekin so'rov baribir oxirigacha boradi: trafik sarflanadi, server ishlaydi. AbortController esa so'rovning o'zini to'xtatadi. fetch ning ikkinchi argumentiga signal beriladi. Cleanup'da controller.abort() chaqiriladi — va fetch qaytargan Promise AbortError bilan rad etiladi.

Jonli blok uchun fetchMenu ga ham signal ni o'rgatamiz — xuddi fetch kabi: abort bo'lsa, taymerni to'xtatib, signal.reason bilan rad etadi:

jsx
import { useEffect, useState } from "react";

const MENU = [
  { id: 1, nom: "Osh", turkum: "taom" },
  { id: 2, nom: "Lag'mon", turkum: "taom" },
  { id: 4, nom: "Ko'k choy", turkum: "ichimlik" },
];

// fetch o'rnida, signal bilan: abort bo'lsa — AbortError
function fetchMenu(category, signal) {
  const delay = category === "taom" ? 600 : 200;
  return new Promise((resolve, reject) => {
    const id = setTimeout(() => {
      resolve(MENU.filter((d) => d.turkum === category));
    }, delay);
    signal.addEventListener("abort", () => {
      clearTimeout(id);
      reject(signal.reason);
    });
  });
}

function App() {
  const [category, setCategory] = useState("taom");
  const [dishes, setDishes] = useState([]);
  useEffect(() => {
    const controller = new AbortController();
    console.log("so'rov:", category);
    fetchMenu(category, controller.signal)
      .then((list) => {
        console.log("javob:", category);
        setDishes(list);
      })
      .catch((error) => {
        if (error.name === "AbortError") {
          console.log("bekor qilindi:", category);
          return;
        }
        throw error;
      });
    return () => controller.abort();
  }, [category]);
  return (
    <div>
      <p>Tanlangan: {category}</p>
      <ul>{dishes.map((d) => <li key={d.id}>{d.nom}</li>)}</ul>
      <button onClick={() => setCategory("ichimlik")}>
        Ichimlik
      </button>
    </div>
  );
}

Konsolda:

text
so'rov: taom
so'rov: ichimlik
bekor qilindi: taom
javob: ichimlik

Ekranda:

text
Tanlangan: ichimlik
Ko'k choy
Ichimlik

Endi taom so'rovi javobini kutmasdan, «Ichimlik» bosilgan zahoti to'xtadi. AbortError — kutilgan holat, xato emas: biz uni catch da tanib, jim chiqamiz. Boshqa xatolar (tarmoq uzildi, server 500) esa yashirilmaydi.

Haqiqiy fetch bilan — xuddi shunday, faqat fetchMenu(category, controller.signal) o'rniga fetch(url, { signal: controller.signal }). Chrome 154 da haqiqiy server bilan tekshirdik (o'sha 1 500 / 300 ms):

text
so'rov: taom
so'rov: ichimlik
bekor qilindi: taom — signal is aborted without reason
javob: ichimlik 1 ta

error.message — "signal sababsiz bekor qilindi": abort() ga sabab berilmagan. DevTools'ning Network panelida bekor qilingan so'rov (canceled) holati bilan ko'rinadi.

ignore bayrog'i AbortController
eski javob ekranga tushadimi yo'q yo'q
eski so'rov to'xtaydimi yo'q, oxirigacha boradi ha
qayerda ishlaydi har qanday Promise fetch va signal qabul qiladigan API
catch da nima hech narsa AbortError ni ajratish

Maslahat: Ikkalasini birga ham ishlatish mumkin. fetch ning javobi kelgandan keyin ham ish bo'lsa (await res.json() dan keyin yana await), signal.aborted ni tekshirish — ignore ning o'zi.

4.4 Effektda ma'lumot olish — bu boshlanishi

Bugungi kod to'g'ri, lekin to'liq emas. "Yuklanmoqda…" holati, xato holati, kesh (ortga qaytganda qayta yuklamaslik), sahifadagi ketma-ket so'rovlar — bularning hammasini qo'lda yozish og'ir. Nega effektda ma'lumot olish ko'pincha eng yaxshi yo'l emasligini Effektda ma'lumot olish darsida ko'ramiz.

5. Vazifalar qadami: tashqi tizimlar effektlarda

5.1 Nima o'zgaradi

refactor/effektlar branch'idagi ishni tugatamiz. useEffect I darsida holat App ga ko'chdi va localStorage effekti yozildi. useEffect II darsida massivlarni tahlil qildik. Bugun — qolgan to'rt tashqi tizim, hammasi cleanup bilan. Keyin asosiy.ts (vanilla tinglovchilar) va render.tsx (ko'prik) o'chadi.

Tashqi tizim v6 / holat 24 (asosiy.ts) holat 28 (App.tsx)
boshqa tablar tingla(boshqaTabdanYangila) — abadiy effekt, cleanup — obunani bekor qiladi
server void sinxronla() modul oxirida effekt, cleanup — AbortController
internet addEventListener("online", …) effekt, cleanup — removeEventListener
URL (Orqaga) addEventListener("popstate", …) effekt, cleanup

5.2 Boshqa tablar: obunani bekor qilish

v6 dagi tingla (sinxron.ts) tinglovchini qo'shardi va hech narsa qaytarmasdi (: void) — vanilla ilovada obuna sahifa yopilguncha yashardi. Komponentda esa obunani to'xtatish kerak. Shuning uchun tingla endi obunani bekor qiluvchi funksiya qaytaradi (kanon, aynan):

diff
-export function tingla(ishlovchi: (xabar: SinxronXabar) => void): void {
-  kanal.addEventListener("message", (event) => {
+export function tingla(
+  ishlovchi: (xabar: SinxronXabar) => void,
+): () => void {
+  const tinglovchi = (event: MessageEvent) => {
     if (xabarmi(event.data)) {
       ishlovchi(event.data);
     }
-  });
+  };
+  kanal.addEventListener("message", tinglovchi);
+  return () => kanal.removeEventListener("message", tinglovchi);
 }

Ishlovchi endi nomli o'zgaruvchida (tinglovchi) — removeEventListener ga aynan o'sha funksiya kerak. Effekt (switch ichi qisqartirildi):

tsx
  // #28: boshqa tablar. Cleanup — obunani bekor qiladi. Kelgan ro'yxat
  // saqlanadi (effekt yuqorida), lekin QAYTA YUBORILMAYDI: yuborsak,
  // tablar bir-biriga cheksiz javob qaytaradi (aks-sado, 17/#30)
  useEffect(
    () =>
      tingla((xabar) => {
        switch (xabar.tur) {
          case OZGARDI: { … setVazifalar(…); … }
        }
      }),
    [],
  );

Arrow funksiya () => tingla(…) — tingla nima qaytarsa, effekt ham shuni qaytaradi. tingla esa () => kanal.removeEventListener("message", tinglovchi) qaytaradi. Demak cleanup — tayyor. Izohdagi "aks-sado" — Sizga effekt kerak emas darsidagi o'lchov mavzusi.

5.3 Server: AbortController

server-sinxron.ts dagi sinxronla har yangi chaqiruvda eski so'rovni bekor qiladi (sinxronPulti?.abort()) va o'zining AbortController ini yaratadi. Effekt uni chaqiradi, cleanup esa yo'ldagi so'rovni to'xtatadi (kanon, aynan):

tsx
  // #28: server — ochilganda bir marta (boshlangichRoyxat o'zgarmaydi).
  // Cleanup so'rovni bekor qiladi (AbortController): komponent yopilsa
  // yoki StrictMode dev'da effektni sinab, qayta ishga tushirsa
  useEffect(() => {
    void sinxronla(boshlangichRoyxat, {
      setVazifalar,
      setTarmoq,
      setTahrirId,
    });
    return sinxronniTox;
  }, [boshlangichRoyxat]);

return sinxronniTox — funksiyaning o'zi (chaqiruv emas). Bekor qilingan so'rovni sinxronla tanib, jim chiqadi: if (xato instanceof ApiXatosi && xato.kod === "BEKOR") return; — «AbortController» bo'limidagi AbortError tekshiruvining kanondagi shakli.

Izohdagi "StrictMode dev'da" ga e'tibor bering. npm run dev da haqiqiy tarmoq jurnalini yozib oldik. Har qatorning boshidagi so'zga qarang — BEKOR yoki 200:

text
  BEKOR GET /api/mashq/vazifalar/de96… — net::ERR_ABORTED      ← 1-effekt, cleanup bekor qildi
  200 GET /api/mashq/vazifalar/de96…
  200 PUT /api/mashq/vazifalar/de96…                         ← server kalitni bilmadi — ro'yxat yuborildi

Bitta ochilishda ikki GET: birinchisi bekor qilingan. Bu StrictMode — u dev'da effektni ataylab "boshla — to'xtat — yana boshla" qiladi. Nega va nima uchun bu foydali — keyingi darsda. Hozircha xulosa: cleanup to'g'ri yozilgani uchun takror zararsiz bo'ldi — ikkinchi PUT yuborilmadi.

5.4 Internet va URL

Brauzer hodisalari — odatdagi juftlik (kanon):

tsx
  // URL: Orqaga/Oldinga — filtr va saralash qayta o'qiladi
  useEffect(() => {
    function orqaga() {
      setFiltr(filtrniOl());
      setSaralash(saralashniOl());
      setTahrirId(null);
    }
    addEventListener("popstate", orqaga);
    return () => removeEventListener("popstate", orqaga);
  }, []);

Internet effekti xuddi shunday tuzilgan (offline va online — ikki tinglovchi, ikkalasi cleanup'da olib tashlanadi), faqat massivi — [vazifalar] (useEffect II darsidagi jadval). pagehide effekti — kutib turgan qoralamani sahifa yopilganda saqlaydi.

5.5 Commit va tekshiruvlar

Uch darsdagi ish (holat App ga, localStorage effekti, massivlar va bugungi to'rt effekt) kanonda bitta commit — biz uni o'qish oson bo'lsin deb uch darsga bo'ldik. asosiy.ts va render.tsx o'chirildi. Fokus yordamchilari fokus.ts ga, server mantiqi server-sinxron.ts ga ko'chdi, holat.ts da faqat sof funksiyalar qoldi.

bash
git add -A
git commit

Commit (kanon): refactor: holat App state'ida, tashqi tizimlar — effektlar. Body: asosiy.ts, render.tsx o'chirildi; main.tsx — kirish. App: useState (vazifalar, filtr, saralash, qidiruv, tahrirId, tarmoq, e'lon, dialog). Effektlar: localStorage, BroadcastChannel (cleanup), server (AbortController), online/offline, popstate, pagehide.

Tekshiruvlar (haqiqiy):

  • npm test — node:test ℹ tests 147 · ℹ pass 147, Vitest Tests 22 passed (22). Holat testlari sof funksiyalarga ko'chdi.
  • npm run lint, format:check, tip — exit 0. Build: dist/assets/index-Dt1vjao_.js 248.41 kB │ gzip: 78.74 kB.
  • Brauzer tekshiruvi — 159/159 birinchi urinishda (oflayn, boshqa tab, dialog, CSP ham).
  • Diff: 18 fayl, +928 −1016 — o'chirilgan qatorlar qo'shilganidan ko'p.

Bitta haqiqiy tuzoq topildi. server-sinxron.ts api ni modul yuklanganda yaratadi va globalThis.fetch ni o'shanda oladi. Vitest'dagi vi.stubGlobal("fetch", …) esa kech qoldi — test haqiqiy fetch bilan undefined/vazifalar/… ga borib, Sinxron bo'lmadi: Internet aloqasini tekshiring. dedi. Soxta fetch vitest-sozlash.ts ga, ya'ni har test faylidan oldin ishlaydigan joyga ko'chirildi (TEXNIK-QARZ 36). Testlar chuqur — Frontend testlash qismida.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
bir bosishda bir necha qator, vaqt o'tib ko'payadi addEventListener bor, cleanup yo'q return () => removeEventListener(…)
yopilgan komponentning taymeri ishlayapti setInterval tozalanmagan return () => clearInterval(id)
tanlangan bitta, ro'yxat boshqa race condition ignore yoki AbortController
konsolda AbortError qizil catch abort'ni ajratmaydi if (error.name === "AbortError") return;
soat 1 da qotdi taymerda setX(x + 1), massiv [] updater: setX((x) => x + 1)
removeEventListener ishlamaydi boshqa funksiya berildi (() => … yangidan) funksiyani o'zgaruvchida saqlang
useEffect must not return anything besides a function useEffect(async () => …) — Promise qaytdi ichida async funksiya e'lon qilib chaqiring

Oxirgi qator haqida batafsil. useEffect(async () => { … }) yozsangiz, effekt funksiyasi Promise qaytaradi. React esa qaytgan qiymatni cleanup deb kutadi va konsolda (qisqartirildi) shunday deydi:

text
useEffect must not return anything besides a function, which is used for clean-up.
It looks like you wrote useEffect(async () => ...) or returned a Promise. Instead, write the async function inside your effect and call it immediately:

"useEffect tozalash uchun ishlatiladigan funksiyadan boshqa hech narsa qaytarmasligi kerak. useEffect(async () => …) yozganga o'xshaysiz. O'rniga async funksiyani effekt ichida yozib, darhol chaqiring." To'g'ri shakl:

jsx
useEffect(() => {
  const controller = new AbortController();
  async function load() {
    const res = await fetch(url, { signal: controller.signal });
    setDishes(await res.json());
  }
  load().catch(() => {}); // xatolarni ushlash — keyinroq to'liq
  return () => controller.abort();
}, [url]);

7. Mashqlar

Mashqlar kurs/mashqlar/17/28-cleanup-race/ da. Har yechimni «Ishga tushir» bilan sinang.

1-mashq (oson): konsol tartibi

Kodni ishga tushirmasdan, «Manti» va keyin «Yopish» bosilgandan keyingi konsolni yozing.

jsx
import { useEffect, useState } from "react";

function Order({ dish }) {
  useEffect(() => {
    console.log("buyurtma:", dish);
    return () => console.log("bekor:", dish);
  }, [dish]);
  return <p>{dish}</p>;
}

function App() {
  const [dish, setDish] = useState("Osh");
  const [open, setOpen] = useState(true);
  return (
    <div>
      {open && <Order dish={dish} />}
      <button onClick={() => setDish("Manti")}>Manti</button>{" "}
      <button onClick={() => setOpen(false)}>Yopish</button>
    </div>
  );
}
  • 2-qator:
  • 4-qator:
Yechim

Konsolda:

text
buyurtma: Osh
bekor: Osh
buyurtma: Manti
bekor: Manti

«Manti» — avval eski effektning cleanup'i (Osh), keyin yangi effekt. «Yopish» — oxirgi effektning cleanup'i. Cleanup har doim o'z effektining qiymatini ko'radi.

2-mashq (o'rta): taymerni tozalang

Buyurtma tayyor bo'lishiga qolgan soniyalarni ko'rsatuvchi komponent. Blok 700 ms kutib, «Bekor qilish» ni bosadi. Hozir komponent yopilgandan keyin ham taymer ishlayveradi va har safar qoldi: yozadi. Effektni tuzating: taymer cleanup'da to'xtasin va to'xtaganini console.log("taymer to'xtadi") bilan bildirsin. Updater haqida «Taymerni to'xtatish» bo'limiga qarang.

jsx
import { useEffect, useState } from "react";

function Countdown() {
  const [left, setLeft] = useState(10);
  useEffect(() => {
    setInterval(() => {
      console.log("qoldi:");
      setLeft((l) => l - 1);
    }, 300);
  }, []);
  return <p>Tayyor bo'lishiga: {left} s</p>;
}

function App() {
  const [open, setOpen] = useState(true);
  return (
    <div>
      {open && <Countdown />}
      <button onClick={() => setOpen(false)}>Bekor qilish</button>
    </div>
  );
}
Yechim
jsx
import { useEffect, useState } from "react";

function Countdown() {
  const [left, setLeft] = useState(10);
  useEffect(() => {
    const id = setInterval(() => {
      console.log("qoldi:");
      setLeft((l) => l - 1);
    }, 300);
    return () => {
      clearInterval(id);
      console.log("taymer to'xtadi");
    };
  }, []);
  return <p>Tayyor bo'lishiga: {left} s</p>;
}

function App() {
  const [open, setOpen] = useState(true);
  return (
    <div>
      {open && <Countdown />}
      <button onClick={() => setOpen(false)}>Bekor qilish</button>
    </div>
  );
}

Konsolda:

text
qoldi:
qoldi:
taymer to'xtadi

Ekranda:

text
Bekor qilish

700 ms ichida ikki "qoldi:" (300 va 600 ms da), keyin cleanup. Oxirgi kut(400) davomida boshqa qator yo'q — taymer haqiqatan to'xtadi. setInterval qaytargan raqamni id ga saqlash shart: busiz to'xtatib bo'lmaydi.

3-mashq (qiyin): taom tafsiloti, poygasiz

DishInfo taom id si bo'yicha ma'lumot oladi. Kodda race condition bor: blok 1 dan keyin tezda 4 ni tanlaydi, 1 ning javobi esa sekinroq keladi. AbortController bilan tuzating: fetchDish(id, signal) ga signal bering, cleanup'da abort qiling va AbortError ni jim o'tkazing (konsolga bekor: 1 yozing). Ekranda 4 ning ma'lumoti qolsin. Ishora: «Ikkinchi davo: AbortController» bo'limidagi fetchMenu — xuddi shunday.

jsx
import { useEffect, useState } from "react";

const DISHES = {
  1: { nom: "Osh", narx: 35000 },
  4: { nom: "Ko'k choy", narx: 5000 },
};

function fetchDish(id, signal) {
  const delay = id === 1 ? 500 : 100; // osh — sekin
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => resolve(DISHES[id]), delay);
    signal?.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    });
  });
}

function App() {
  const [id, setId] = useState(1);
  const [dish, setDish] = useState(null);
  useEffect(() => {
    fetchDish(id).then((data) => {
      console.log("javob:", id);
      setDish(data);
    });
  }, [id]);
  const text = dish
    ? `${dish.nom} — ${dish.narx} so'm`
    : "Yuklanmoqda…";
  return (
    <div>
      <p>{text}</p>
      <button onClick={() => setId(4)}>Ko'k choy</button>
    </div>
  );
}

Konsolda:

text
javob: 4
javob: 1

Ekranda:

text
Osh — 35000 so'm
Ko'k choy
Yechim
jsx
import { useEffect, useState } from "react";

const DISHES = {
  1: { nom: "Osh", narx: 35000 },
  4: { nom: "Ko'k choy", narx: 5000 },
};

function fetchDish(id, signal) {
  const delay = id === 1 ? 500 : 100; // osh — sekin
  return new Promise((resolve, reject) => {
    const timer = setTimeout(() => resolve(DISHES[id]), delay);
    signal?.addEventListener("abort", () => {
      clearTimeout(timer);
      reject(signal.reason);
    });
  });
}

function App() {
  const [id, setId] = useState(1);
  const [dish, setDish] = useState(null);
  useEffect(() => {
    const controller = new AbortController();
    fetchDish(id, controller.signal)
      .then((data) => {
        console.log("javob:", id);
        setDish(data);
      })
      .catch((error) => {
        if (error.name === "AbortError") {
          console.log("bekor:", id);
          return;
        }
        throw error;
      });
    return () => controller.abort();
  }, [id]);
  const text = dish
    ? `${dish.nom} — ${dish.narx} so'm`
    : "Yuklanmoqda…";
  return (
    <div>
      <p>{text}</p>
      <button onClick={() => setId(4)}>Ko'k choy</button>
    </div>
  );
}

Konsolda:

text
bekor: 1
javob: 4

Ekranda:

text
Ko'k choy — 5000 so'm
Ko'k choy

1 ning so'rovi «Ko'k choy» bosilgan zahoti to'xtadi — uning javobi umuman kelmadi. Shuni ham sezing: tanlov almashganda eski taom bir lahza ekranda qoladi, keyin yangisi keladi. "Yuklanmoqda…" ni har almashishda ko'rsatish kerakmi — dizayn qarori; Effektda ma'lumot olish darsida yuklanish holatini to'liq yozamiz.

4-mashq: Vazifalar qadami

refactor/effektlar branch'ida «Vazifalar qadami: tashqi tizimlar effektlarda» bo'limidagi to'rt effektni yozing, asosiy.ts va render.tsx ni o'chiring, commit qiling va PR oching. npm run check va npm run build toza bo'lsin.

Yechim

Tekshiruv ro'yxati:

  • grep -rn "addEventListener" src/App.tsx — har biriga juft removeEventListener bor.
  • npm run dev — DevTools Network'da bitta ochilishda ikki GET, birinchisi (canceled): cleanup ishlayapti.
  • Ikki tab oching, birida vazifa belgilang — ikkinchisida darhol ko'rinadi; tabni yopsangiz, konsolda xato yo'q.
  • npm test — node 147, Vitest 22. Brauzer tekshiruvi — 159/159.

Commit — refactor: holat App state'ida, tashqi tizimlar — effektlar. PR'ni birlashtiring.

8. Real ishda

  • Xotira oqishi (memory leak) — tozalanmagan tinglovchi va taymer React ilovalarida eng ko'p uchraydigan sekinlik sababi. Chrome DevTools'ning Memory paneli va getEventListeners(window) (faqat DevTools konsolida) ularni topadi.
  • AbortController — fetch, addEventListener ({ signal } opsiyasi bilan), axios va ko'p kutubxonalar uni qabul qiladi. Bitta controller.abort() bir nechta tinglovchi va so'rovni birdan to'xtatadi.
  • Data-fetching kutubxonalari (TanStack Query, SWR) ichida aynan shu cleanup va bekor qilish bor (TanStack Query I).
  • Intervyu savollari: "useEffect cleanup qachon chaqiriladi?", "React'da race condition nima va qanday oldini olasiz?", "ignore bayrog'i va AbortController farqi?".

Xulosa

  • Effekt funksiyasi qaytargan funksiya — cleanup. React uni effekt qayta ishlashidan oldin va komponent olib tashlanganda chaqiradi.
  • Effekt nima boshlasa — cleanup o'shani to'xtatadi: clearInterval, removeEventListener, obunani bekor qilish, abort().
  • Cleanup o'z effektining qiymatlarini ko'radi (closure) — juftlik: setup va cleanup.
  • Race condition: kech yuborilgan so'rov oldin keladi. Davo — ignore bayrog'i (javobni tashlaydi) yoki AbortController (so'rovni to'xtatadi).
  • vazifalar: boshqa tablar, server, internet va URL — cleanup'li effektlar; 159/159, StrictMode dev'da birinchi GET bekor qilinadi.

Keyingi dars: Effekt hayot sikli va StrictMode'dagi ikki marta ishlash — "mount/unmount" emas, "boshla — to'xtat" deb o'ylash va StrictMode nega effektni dev'da ikki marta ishga tushiradi.

Manbalar

  • React: useEffect — "Connecting to an external system", "Fetching data with Effects" — react.dev/reference/react/useEffect
  • React: "Synchronizing with Effects" — "How to handle the Effect firing twice in development?" — react.dev/learn/synchronizing-with-effects
  • MDN: AbortController, AbortSignal — developer.mozilla.org/docs/Web/API/AbortController
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
useEffect III: cleanup, race condition va AbortController — effekt boshlagan ishni tozalash — IlmHamroh