IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar18/36-dars26 daqiqa
Mundarija (29)

React real-time: WebSocket, SSE va Socket.IO bilan jonli ma'lumot

Qisqacha: Server o'zi xabar yuborishi uchun brauzer doimiy ulanish ochadi: WebSocket (ikki tomonlama), SSE — EventSource (faqat serverdan) yoki Socket.IO (WebSocket ustidagi kutubxona). React'da ulanish useEffect da ochiladi va cleanup'da yopiladi. Kelgan xabar TanStack Query keshiga tushadi: ma'lumotning o'zi bo'lsa — setQueryData, faqat "o'zgardi" signali bo'lsa — invalidateQueries.

Bu darsda

  • Polling, SSE va WebSocket'ni solishtirasiz va vaziyatga qarab tanlaysiz.
  • Ulanishni useEffect + cleanup bilan to'g'ri ochib-yopasiz (StrictMode'dagi ikki marta ulanish bilan).
  • Xabarni Query keshiga ikki usulda ulaysiz: setQueryData va invalidateQueries.
  • Qayta ulanish (eksponensial kutish) va ulanish holati indikatorini yozasiz.
  • vazifalar ga dev server orqali real-time qo'shasiz: boshqa qurilmadagi o'zgarish 254 ms da.

Oldin bilishingiz kerak: WebSocket va Server-Sent Events, TanStack Query III: mutatsiyalar, useEffect III: cleanup va AbortController, TanStack Query II: kesh hayot sikli.

1. Nega bu kerak?

«Bahor» oshxonasida planshet turibdi: yangi buyurtmalar ro'yxati. Ofitsiant zalda buyurtma kiritdi — oshpaz uni darhol ko'rishi kerak. 30 soniya keyin emas.

TanStack Query II da vazifalar uchun staleTime: 30_000 tanladik va narxini halol aytdik: boshqa qurilmadagi o'zgarish 30 soniyagacha kechikadi. Vazifalar ro'yxatida bu zararsiz. Oshxonada, chatda, kuryer xaritasida, birja narxlarida — zarar.

Yechimlar ikki xil. Birinchisi — mijoz tez-tez so'raydi: "yangi narsa bormi?" Ikkinchisi — server o'zi aytadi: "yangi buyurtma keldi". O'xshatish: pochta va telefon. Pochta qutisini har 5 daqiqada tekshirish mumkin (polling) — ko'pincha bo'sh. Yoki telefon qilib qo'yishadi — faqat xabar bo'lganda (doimiy ulanish).

WebSocket va Server-Sent Events darsida brauzer API'larini o'rgandik. Bugun — ularni React va Query bilan to'g'ri ulash.

2. Uch usul: polling, SSE, WebSocket

2.1 Polling — refetchInterval

Eng oddiy "real-time" — vaqt bo'yicha so'rash. Query'da bitta sozlama: refetchInterval. Natija oynasida kutmaslik uchun oraliq 150 ms (haqiqiy ilovada 3–10 s):

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

const queryClient = new QueryClient();

let orders = 0;
let requests = 0;

function fetchOrders() {
  requests += 1;
  if (requests === 3) orders += 1; // faqat bir marta yangi buyurtma
  return Promise.resolve({ orders, requests });
}

function Kitchen() {
  const { data } = useQuery({
    queryKey: ["orders"],
    queryFn: fetchOrders,
    refetchInterval: 150,
  });
  if (data === undefined) return <p>Yuklanmoqda…</p>;
  return (
    <p>
      Yangi buyurtmalar: {data.orders}
      {data.requests >= 5 && " (so'rovlar: 5+)"}
    </p>
  );
}

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

Ekranda:

text
Yangi buyurtmalar: 1 (so'rovlar: 5+)

Kamida beshta so'rov — va faqat bittasi yangi narsa olib keldi. Qolganlari "hech narsa o'zgarmadi" javobi uchun trafik va server vaqti. Shunga qaramay polling yomon emas: oddiy, har qanday server bilan ishlaydi, kesh va staleTime bilan mos. Ma'lumot kamdan-kam o'zgarsa yoki 5–10 soniyalik kechikish zararsiz bo'lsa — yetarli.

Bitta foydali detal: refetchIntervalInBackground sukut bo'yicha false. Foydalanuvchi boshqa tabga o'tsa, polling to'xtaydi — ko'rmayotgan ekran uchun so'ramaymiz.

2.2 SSE va WebSocket

Polling SSE (EventSource) WebSocket
yo'nalish mijoz so'raydi server → mijoz ikki tomonlama
kechikish oraliq (3–10 s) darhol darhol
protokol oddiy HTTP oddiy HTTP (text/event-stream) ws://, wss://
qayta ulanish kerak emas brauzer o'zi qo'lda
server talabi hech narsa doimiy javobni ushlab turish WebSocket server
misol statistika paneli bildirishnomalar, yangiliklar chat, o'yin, hamkorlikdagi tahrir

Qaror savoli: mijoz ham serverga tez-tez yozadimi? Chatda — ha, har xabar. Oshxona planshetida — yo'q, u faqat tinglaydi; buyurtmani ofitsiant oddiy POST bilan yuboradi. Bir tomonlama oqim uchun SSE yetarli va soddaroq: oddiy HTTP, proksi va CDN'lardan o'tadi, qayta ulanish tayyor.

flowchart TD
  A["Ma'lumot tez o'zgaradimi?"] -->|"yo'q"| P["staleTime / polling"]
  A -->|"ha"| B["Mijoz ham tez-tez yozadimi?"]
  B -->|"yo'q"| S["SSE"]
  B -->|"ha"| W["WebSocket / Socket.IO"]

3. Ulanish — effekt

3.1 Ochish va yopish

Doimiy ulanish — React uchun tashqi tizim. useEffect I darsidagi qoida: tashqi tizim bilan sinxronlash — effektda, tozalash — cleanup'da.

Natija oynasida haqiqiy WebSocket va EventSource ishlamaydi (tarmoq yo'q). Shuning uchun misollarda soxta ulanish — FakeSocket. Interfeysi WebSocket'nikiga o'xshash: addEventListener("open" | "message" | "close") va close(). Ichida u "server" bo'lib, har 150 ms da buyurtma yuboradi:

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

// Soxta ulanish: WebSocket kabi hodisalar, ichida taymer
class FakeSocket extends EventTarget {
  constructor(url) {
    super();
    console.log("ulanmoqda:", url);
    let n = 0;
    this.timer = setInterval(() => {
      n += 1;
      const data = JSON.stringify({ order: `#${100 + n}` });
      this.dispatchEvent(new MessageEvent("message", { data }));
    }, 150);
  }
  close() {
    clearInterval(this.timer);
    this.dispatchEvent(new Event("close"));
  }
}

function Kitchen() {
  const [orders, setOrders] = useState([]);
  useEffect(() => {
    const socket = new FakeSocket("wss://example.com/oshxona");
    const onMessage = (event) => {
      const { order } = JSON.parse(event.data);
      setOrders((list) => [...list, order]);
    };
    socket.addEventListener("message", onMessage);
    socket.addEventListener("close", () => console.log("uzildi"));
    return () => socket.close();
  }, []);
  return <p>Buyurtmalar: {orders.join(", ")}</p>;
}

function Page() {
  const [on, setOn] = useState(true);
  return (
    <>
      <button onClick={() => setOn(false)}>Uzish</button>
      {on && <Kitchen />}
    </>
  );
}

export default function App() {
  return <Page />;
}

Konsolda:

text
ulanmoqda: wss://example.com/oshxona
uzildi

Ekranda:

text
Uzish

Uch narsa:

  • Bog'liqlik [] — ulanish komponent ekranga chiqqanda bir marta. Manzil prop'dan kelsa — [url]: manzil o'zgarsa, eski ulanish yopiladi, yangisi ochiladi.
  • Cleanup — socket.close(). Komponent yopilganda ulanish yopiladi va konsolda "uzildi". Cleanup bo'lmasa, sahifadan chiqib ketgan foydalanuvchining ulanishi ochiq qolardi. Har kirishda yana bittasi — ulanishlar va xabarlar ko'payib boradi.
  • Updater funksiya — setOrders((list) => [...list, order]). Ishlovchi effekt ichida bir marta yaratilgan. Unda orders to'g'ridan-to'g'ri ishlatilsa — doim boshlang'ich bo'sh massiv (State II: snapshot va updater, eskirgan closure).

3.2 StrictMode: ikki marta ulanish

Dev rejimida StrictMode effektni "sinab ko'radi": o'rnatadi, darhol tozalaydi, yana o'rnatadi (Effekt hayot sikli va StrictMode). Real-time'da bu ko'rinadi:

jsx
import { StrictMode, useEffect } from "react";
import { createRoot } from "react-dom/client";

class FakeSocket extends EventTarget {
  constructor(url) {
    super();
    console.log("ulanmoqda:", url);
  }
  close() {
    console.log("yopildi");
  }
}

function Kitchen() {
  useEffect(() => {
    const socket = new FakeSocket("wss://example.com/oshxona");
    return () => socket.close();
  }, []);
  return <p>Oshxona ekrani</p>;
}

createRoot(document.getElementById("root")).render(
  <StrictMode>
    <Kitchen />
  </StrictMode>,
);

Konsolda:

text
ulanmoqda: wss://example.com/oshxona
yopildi
ulanmoqda: wss://example.com/oshxona

Ochildi, yopildi, yana ochildi — oxirida bitta ochiq ulanish. Bu xato emas, cleanup to'g'ri ishlayotganining isboti. Server loglarida dev'da har sahifa ochilishida ikki ulanish ko'rinadi — production'da bitta. vazifalar ning o'lchovida ham shunday yozilgan: "EventSource so'rovlari: A 1, B 1 — StrictMode".

Xato — cleanup'ni yozmaslik. Unda StrictMode ikkita ochiq ulanish qoldiradi va har xabar ikki marta keladi. StrictMode aynan shu xatoni dev'da ko'rsatish uchun bor.

Tekshirib ko'ring: useEffect(() => { const s = new WebSocket(url); s.onmessage = …; }, [url]) — cleanup yo'q. Foydalanuvchi uch xil xonani (url) ketma-ket ochdi. Nechta ulanish ochiq va xabarlar qaysi xonadan keladi?

Javob

Uchta ulanish ochiq (dev'da StrictMode bilan — oltita). Xabarlar uchala xonadan keladi va hammasi ekrandagi bitta ro'yxatga tushadi: foydalanuvchi 3-xonada turib, 1-xonaning xabarlarini ko'radi. Davosi — return () => s.close();.

4. Xabar — Query keshiga

4.1 Ma'lumot xabarda: setQueryData

Oldingi misolda buyurtmalar useState da edi. Lekin ular server ma'lumoti: ro'yxatning boshi useQuery bilan serverdan keladi, yangilari — ulanishdan. Ikkalasi bitta joyda bo'lishi kerak — Query keshida. Xabar to'liq buyurtmani olib kelsa, uni TanStack Query III dagi setQueryData bilan keshga qo'shamiz:

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

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: Infinity } },
});

class FakeSocket extends EventTarget {
  constructor() {
    super();
    let n = 0;
    this.timer = setInterval(() => {
      n += 1;
      if (n > 2) return;
      const data = JSON.stringify({ id: 100 + n, dish: "Manti" });
      this.dispatchEvent(new MessageEvent("message", { data }));
    }, 150);
  }
  close() {
    clearInterval(this.timer);
  }
}

function fetchOrders() {
  console.log("GET buyurtmalar");
  return Promise.resolve([{ id: 100, dish: "Osh" }]);
}

function useLiveOrders() {
  const client = useQueryClient();
  useEffect(() => {
    const socket = new FakeSocket();
    socket.addEventListener("message", (event) => {
      const order = JSON.parse(event.data);
      client.setQueryData(["orders"], (old = []) => [...old, order]);
    });
    return () => socket.close();
  }, [client]);
}

function Kitchen() {
  useLiveOrders();
  const { data } = useQuery({
    queryKey: ["orders"],
    queryFn: fetchOrders,
  });
  return (
    <ul>
      {data?.map((o) => (
        <li key={o.id}>
          #{o.id} — {o.dish}
        </li>
      ))}
    </ul>
  );
}

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

Konsolda:

text
GET buyurtmalar

Ekranda:

text
#100 — Osh
#101 — Manti
#102 — Manti

Bitta GET — boshlang'ich ro'yxat. Keyingi ikki buyurtma serverga so'rovsiz, ulanishdan keldi. staleTime: Infinity — "bu ma'lumot o'z-o'zidan eskirmaydi": yangilanishni ulanish beradi, oynaga qaytganda qayta so'rash shart emas.

Ulanish mantig'i alohida custom hook'da (useLiveOrders) — komponent faqat uni chaqiradi (Custom hooklar I). Boshqa komponentlar ham ["orders"] ni o'qisa — ular ham yangi buyurtmani ko'radi: kesh bitta.

4.2 Faqat signal: invalidateQueries

Ko'pincha xabarda ma'lumot yo'q, faqat "o'zgardi" degan signal bor. Yoki ma'lumot bor, lekin uni keshga to'g'ri joylash murakkab: tartib, filtr, sahifalar. Unda xabar kelganda keshni eskirgan deb belgilaymiz — Query o'zi serverdan yangisini oladi:

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

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: Infinity } },
});

const serverOrders = ["Osh"];

class FakeSocket extends EventTarget {
  constructor() {
    super();
    this.timer = setTimeout(() => {
      serverOrders.push("Lag'mon"); // boshqa qurilma qo'shdi
      const event = new MessageEvent("message", { data: "ozgardi" });
      this.dispatchEvent(event);
    }, 200);
  }
  close() {
    clearTimeout(this.timer);
  }
}

function fetchOrders() {
  console.log("GET");
  return Promise.resolve([...serverOrders]);
}

function useLiveInvalidation() {
  const client = useQueryClient();
  useEffect(() => {
    const socket = new FakeSocket();
    socket.addEventListener("message", () => {
      console.log("signal keldi");
      client.invalidateQueries({ queryKey: ["orders"] });
    });
    return () => socket.close();
  }, [client]);
}

function Kitchen() {
  useLiveInvalidation();
  const { data } = useQuery({
    queryKey: ["orders"],
    queryFn: fetchOrders,
  });
  return <p>Buyurtmalar: {data?.join(", ")}</p>;
}

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

Konsolda:

text
GET
signal keldi
GET

Ekranda:

text
Buyurtmalar: Osh, Lag'mon
setQueryData invalidateQueries
xabarda to'liq ma'lumot faqat "o'zgardi"
so'rov yo'q har signalda bitta GET
to'g'rilik xabar keshga to'g'ri qo'shilsa doim server haqiqati
murakkab kesh (filtr, sahifa) qiyin oson

vazifalar ikkinchisini tanladi — «Vazifalar qadami» bo'limida. Sabab: «Bahor» mashq serveri hodisa yubormaydi, signalni dev server yasaydi va unda ma'lumot yo'q.

Diqqat: O'z xabaringizga ehtiyot bo'ling. Siz vazifa qo'shdingiz → server hamma tinglovchiga "o'zgardi" deydi → sizga ham. Siz mutatsiyadan keyin allaqachon yangiladingiz, endi yana GET. Yoki undan yomoni: optimistik qatlam ustiga eski ma'lumot keladi. Davosi — xabarda "kim yozdi" belgisi va o'zingiznikini e'tiborsiz qoldirish. vazifalar da bu — X-Jonli-Manba sarlavhasi.

5. Ulanish uziladi: qayta ulanish va holat

5.1 Qayta ulanish

Telefon metroga tushdi, Wi-Fi almashdi, server qayta ishga tushdi — ulanish uziladi. Kim qayta ulaydi?

  • EventSource — brauzer o'zi. Server retry: 2000 yuborsa, uzilgandan 2 s keyin qayta urinadi.
  • WebSocket — hech kim. close hodisasini tinglab, o'zingiz yangisini ochasiz.
  • Socket.IO — kutubxona o'zi, sozlanadigan kutish bilan.

Qo'lda qayta ulashda TanStack Query V dagi eksponensial kutish ishlatiladi. Unga tasodifiy qo'shimcha (jitter) qo'shiladi: server qayta ishga tushganda minglab mijoz aynan bir soniyada urilmasin. Tasodifiy natijani tekshirib bo'lmaydi, shuning uchun bu misolda jitter — parametr:

js
// Qayta ulanish oralig'i: 0.5 s, 1 s, 2 s … 30 s gacha + jitter
function reconnectDelay(attempt, jitter) {
  const base = Math.min(500 * 2 ** attempt, 30000);
  return Math.round(base * (1 + jitter * 0.3));
}

for (let attempt = 0; attempt < 8; attempt++) {
  // haqiqiy kodda: Math.random() (0…1)
  const min = reconnectDelay(attempt, 0);
  const max = reconnectDelay(attempt, 1);
  console.log(attempt + 1, min, max);
}

Konsolda:

text
1 500 650
2 1000 1300
3 2000 2600
4 4000 5200
5 8000 10400
6 16000 20800
7 30000 39000
8 30000 39000

Har qatorda — urinish raqami, jitter'siz va eng katta jitter bilan kutish (ms). Haqiqiy kodda jitter o'rniga Math.random(): har mijoz o'z vaqtida. Muvaffaqiyatli ulanishdan keyin attempt nolga qaytadi.

5.2 Ulanish holati — foydalanuvchiga

Oshpaz planshetga qaradi: buyurtma yo'q. Haqiqatan yo'qmi yoki ulanish uzilganmi? Ko'rinmasa — ikkalasi bir xil ko'rinadi. Shuning uchun real-time ekranda ulanish holati indikatori bo'lishi kerak:

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

// Soxta ulanish: 300 ms da ochiladi, lifetime ms da uziladi
class FakeSocket extends EventTarget {
  constructor(lifetime) {
    super();
    this.t1 = setTimeout(
      () => this.dispatchEvent(new Event("open")),
      300,
    );
    this.t2 = setTimeout(
      () => this.dispatchEvent(new Event("close")),
      lifetime,
    );
  }
  close() {
    clearTimeout(this.t1);
    clearTimeout(this.t2);
  }
}

function useConnection() {
  const [status, setStatus] = useState("ulanmoqda");
  const [attempt, setAttempt] = useState(0);
  useEffect(() => {
    // 1-ulanish 900 ms yashaydi, keyingisi — uzoq
    const socket = new FakeSocket(attempt === 0 ? 900 : 60000);
    let timer;
    socket.addEventListener("open", () => setStatus("jonli"));
    socket.addEventListener("close", () => {
      setStatus("uzildi");
      timer = setTimeout(() => {
        setStatus("ulanmoqda");
        setAttempt((a) => a + 1); // effekt qayta ishlaydi
      }, 600);
    });
    return () => {
      clearTimeout(timer);
      socket.close();
    };
  }, [attempt]);
  return status;
}

const LABELS = {
  ulanmoqda: "⏳ Ulanmoqda…",
  jonli: "🟢 Jonli",
  uzildi: "🔴 Uzildi — qayta ulanmoqda",
};

export default function App() {
  const status = useConnection();
  return <p role="status">{LABELS[status]}</p>;
}

Ekranda:

text
🟢 Jonli

Oraliq nuqtalar butun yo'lni tekshirdi: "Ulanmoqda" → "Jonli" → "Uzildi" → (600 ms) → qayta "Ulanmoqda" → "Jonli". Qayta ulanish — attempt state'ini oshirish: u effekt bog'liqligida, demak eski ulanish tozalanadi va yangisi ochiladi.

role="status" — Malika (ekran o'quvchi) holat o'zgarganini eshitadi (React'da accessibility). Indikatorni faqat rang bilan bermang: rang ko'rmaydigan foydalanuvchi uchun matn ham kerak.

Internet umuman yo'qligi — boshqa narsa. Uni navigator.onLine bilan bilamiz (useSyncExternalStore darsidagi useOnlayn). Query ham buni biladi (onlineManager): internet qaytganda refetchOnReconnect keshni yangilaydi. Ya'ni ulanish uzilgan paytda o'tkazib yuborilgan xabarlar yo'qolmaydi — qayta ulanishda bir marta invalidateQueries qiling yoki Query'ning o'ziga ishoning.

6. Haqiqiy API'lar: kod qanday ko'rinadi

Natija oynasida ishlamaydigan, lekin loyihada aynan shunday yoziladigan kod. Uchalasi ham bitta naqsh: effektda ochish, xabarda keshni yangilash, cleanup'da yopish.

WebSocket:

jsx
useEffect(() => {
  const socket = new WebSocket("wss://example.com/oshxona");
  socket.addEventListener("message", (event) => {
    const order = JSON.parse(event.data);
    client.setQueryData(["orders"], (old = []) => [...old, order]);
  });
  // yuborish ham mumkin: socket.send(JSON.stringify({ ... }))
  return () => socket.close();
}, [client]);

SSE (EventSource):

jsx
useEffect(() => {
  const source = new EventSource("/api/jonli");
  // server: "event: ozgardi\ndata: {...}\n\n"
  source.addEventListener("ozgardi", () => {
    client.invalidateQueries({ queryKey: ["orders"] });
  });
  return () => source.close();
}, [client]);

Socket.IO — WebSocket ustidagi kutubxona: qayta ulanish, nomlangan hodisalar, xonalar (rooms), WebSocket ishlamasa — HTTP long-polling'ga o'tish. Shart: server ham Socket.IO bo'lishi kerak — oddiy WebSocket server bilan gaplashmaydi:

jsx
// o'z loyihangizda: npm i socket.io-client
import { io } from "socket.io-client";

useEffect(() => {
  const socket = io("https://example.com");
  socket.on("buyurtma:yangi", (order) => {
    client.setQueryData(["orders"], (old = []) => [...old, order]);
  });
  socket.emit("xona:kirish", "oshxona");
  return () => socket.disconnect();
}, [client]);

Socket.IO serverini Socket.IO darsida yozamiz. Frontendda tanlov oddiy: backend nima bersa — shuni ishlatasiz.

Maslahat: Bitta sahifada bir nechta komponent bir xil ulanishga muhtoj bo'lsa, har biri o'z ulanishini ochmasin. Ulanishni bitta joyda (layout yoki Provider) oching, xabarlarni Query keshiga yozing — komponentlar keshdan o'qiydi. vazifalar da ulanish Qobiq da — butun ilovaga bitta.

7. Vazifalar qadami: boshqa qurilmadagi o'zgarish — SSE (holat 18)

7.1 Muammo va cheklov

Ikki qurilma, bitta ro'yxat: telefonda vazifa belgilandi — kompyuterda 30 soniyagacha ko'rinmaydi (TanStack Query II o'lchovining 6-qadami). Real-time kerak.

Cheklov: «Bahor» mashq serveri hodisa yubormaydi — bizning backend'imiz hali yo'q (25-qism). Production (GitHub Pages, Vercel) — statik hosting, doimiy ulanishni ushlab turadigan server yo'q.

Kanon yechimi: real-time faqat dev server'da. npm run dev da Vite proksi orqali «Bahor» API'siga so'rovlar o'tadi (Dev server proxy darsida sozlagan edik; plagin yozishni — Vite plaginlari darsida). Proksi yozuvni (PATCH, POST, DELETE) ko'rgan zahoti, shu kalitni tinglayotgan brauzerlarga SSE hodisasi yuboradi. Brauzer — invalidateQueries. Production build'da bu kod umuman yo'q.

sequenceDiagram
  participant A as Qurilma A
  participant V as Vite dev server
  participant S as «Bahor» API
  participant B as Qurilma B
  B->>V: GET /api/jonli/kalit (SSE, ochiq)
  A->>V: PATCH (X-Jonli-Manba: A)
  V->>S: proksi
  S-->>V: 200
  V-->>A: 200
  V-->>B: event: ozgardi
  B->>V: GET ro'yxat (invalidateQueries)

Nega WebSocket emas: xabar bir tomonlama — serverdan brauzerga. Yozuvlar oddiy HTTP so'rovlar bilan ketaveradi. SSE — oddiy HTTP javob, Vite middleware'da 20 qatorda yoziladi va brauzer o'zi qayta ulanadi.

bash
git switch -c feat/jonli-dev

7.2 Server tomoni: Vite plagini (skriptlar/jonli.js)

Kanon (izohlar qisqartirilgan qatorlarga bo'lingan):

js
// skriptlar/jonli.js — real-time yangilanish, FAQAT dev
// server'da (19/#18). Server-Sent Events: brauzer
// GET /api/jonli/<kalit> ochib qo'yadi, dev server unga
// "ozgardi" hodisasini yuboradi — shu kalitli ro'yxatga
// (dev proxy orqali) yozuv o'tganda. «Bahor» mashq serveri
// hodisa yubormaydi — shuning uchun hodisani proxy'ning o'zi
// yasaydi. Production'da (Pages, Vercel) YO'Q: statik
// hostingda doimiy ulanish ushlab turadigan server yo'q —
// 25-qismda o'z backend'imiz bilan
const JONLI = "/api/jonli/";
const YOZUV = /^\/api\/mashq\/vazifalar\/([^/?]+)/;

/**
 * @returns {import("vite").Plugin}
 */
export function jonliPlagini() {
  /** @type {Map<string, Set<import("node:http").ServerResponse>>} */
  const tinglovchilar = new Map();

  /**
   * @param {string} kalit
   * @param {string} manba yozgan tab (o'zi o'z hodisasini
   *   e'tiborsiz qoldiradi)
   */
  function yubor(kalit, manba) {
    for (const javob of tinglovchilar.get(kalit) ?? []) {
      javob.write(
        `event: ozgardi\ndata: ${JSON.stringify({ manba })}\n\n`,
      );
    }
  }

  return {
    name: "vazifalar-jonli",
    apply: "serve",
    configureServer(server) {
      // Vite'ning o'z middleware'laridan (proxy ham) OLDIN
      // ishlaydi
      server.middlewares.use((req, res, keyingi) => {
        const url = req.url ?? "";
        if (req.method === "GET" && url.startsWith(JONLI)) {
          const kalit = decodeURIComponent(url.slice(JONLI.length));
          res.writeHead(200, {
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-store",
            Connection: "keep-alive",
          });
          // retry — uzilsa brauzer necha ms dan keyin qayta
          // ulanadi
          res.write("retry: 2000\n\n");
          const toplam = tinglovchilar.get(kalit) ?? new Set();
          toplam.add(res);
          tinglovchilar.set(kalit, toplam);
          req.on("close", () => toplam.delete(res));
          return;
        }
        const mos = YOZUV.exec(url);
        if (
          mos !== null &&
          req.method !== "GET" &&
          mos[1] !== undefined
        ) {
          const kalit = decodeURIComponent(mos[1]);
          const manba = String(req.headers["x-jonli-manba"] ?? "");
          // Proxy javobni qaytargach — muvaffaqiyatli bo'lsa,
          // hodisa
          res.on("finish", () => {
            if (res.statusCode < 400) {
              yubor(kalit, manba);
            }
          });
        }
        keyingi();
      });
    },
  };
}

WebSocket va SSE darsidagi SSE formati shu yerda: Content-Type: text/event-stream, har hodisa — event: va data: qatorlari, oxirida bo'sh qator (\n\n). Qolgani:

  • apply: "serve" — plagin faqat npm run dev da. npm run build uni ko'rmaydi.
  • tinglovchilar — kalit → ochiq javoblar to'plami. Ikki qurilma bir kalit bilan — bitta to'plamda.
  • req.on("close") — brauzer tabni yopdi → javob to'plamdan chiqadi. Server tomonidagi cleanup.
  • res.on("finish") — proksi «Bahor» javobini qaytargach, statusCode < 400 bo'lsa — hodisa. Rad etilgan yozuv haqida hech kimni ogohlantirmaymiz.

vite.config.ts da bitta qator: jonliPlagini() plaginlar ro'yxatiga. .env.development da — VITE_JONLI_URL=/api/jonli.

7.3 Brauzer tomoni: src/jonli.ts

Kanon (izohlar bo'lingan):

ts
// jonli.ts — real-time yangilanish (19/#18, Server-Sent
// Events). FAQAT dev'da: VITE_JONLI_URL — .env.development da,
// production'da yo'q (Vite uni undefined bilan almashtiradi va
// bu kod build'ga tushmaydi). Boshqa qurilmada o'zgarish bo'ldi
// → ro'yxat so'rovi eskirgan deb belgilanadi → Query serverdan
// qayta so'raydi (ma'lumot hodisada emas)
import { useEffect } from "react";
import { useQueryClient } from "@tanstack/react-query";
import { JONLI_MANBA } from "./jonli-manba.ts";
import { kalitniOl } from "./saqlash.ts";
import { royxatKaliti } from "./server-sinxron.ts";

export function useJonliYangilanish(): void {
  const mijoz = useQueryClient();
  useEffect(() => {
    const manzil = import.meta.env.VITE_JONLI_URL;
    if (manzil === undefined) {
      return undefined;
    }
    // EventSource o'zi qayta ulanadi (server "retry: 2000"
    // aytgan)
    const manba = new EventSource(`${manzil}/${kalitniOl()}`);
    const ozgardi = (event: MessageEvent<string>) => {
      const malumot: unknown = JSON.parse(event.data);
      const ozimizmi =
        typeof malumot === "object" &&
        malumot !== null &&
        "manba" in malumot &&
        malumot.manba === JONLI_MANBA;
      if (!ozimizmi) {
        void mijoz.invalidateQueries({ queryKey: royxatKaliti });
      }
    };
    manba.addEventListener("ozgardi", ozgardi);
    return () => manba.close();
  }, [mijoz]);
}

Bugungi darsning hammasi: effekt + cleanup (manba.close()), nomlangan hodisa ("ozgardi"), invalidateQueries, o'z xabarini tanish. Xabar unknown deb o'qiladi va qadam-baqadam toraytiriladi (Type narrowing) — tashqaridan kelgan JSON'ga ishonmaymiz.

JONLI_MANBA — tabning tasodifiy belgisi (jonli-manba.ts):

ts
// jonli-manba.ts — shu tabning belgisi (19/#18): dev'da yozuv
// sarlavhasida ketadi, jonli hodisada qaytadi — tab o'z
// yozuvini taniydi (server-sinxron.ts va jonli.ts — aylana
// import bo'lmasin, alohida modul)
export const JONLI_MANBA = crypto.randomUUID();

U yozuvlar bilan X-Jonli-Manba sarlavhasida ketadi. api.ts ga ixtiyoriy yozuvSarlavhalari qo'shildi va server-sinxron.ts faqat dev'da beradi (kanon):

ts
  // 19/#18: dev'da yozuv qaysi tabdan — jonli hodisa o'ziga
  // qaytsa, tab uni taniydi (jonli.ts). Production'da yo'q
  // (VITE_JONLI_URL yo'q)
  ...(import.meta.env.VITE_JONLI_URL === undefined
    ? {}
    : { yozuvSarlavhalari: { "X-Jonli-Manba": JONLI_MANBA } }),

Nega faqat dev'da? api.ts izohi aytadi: begona serverga nostandart sarlavha CORS preflight talab qiladi. Production'da so'rov to'g'ridan «Bahor» ga ketadi — keraksiz OPTIONS so'rovi bo'lardi. Dev'da esa so'rov o'z origin'imizga (proksi).

Hook butun ilova uchun bir marta — Qobiq da (kanon):

diff
   useTarixQadami();
+  // 19/#18: boshqa qurilmadagi o'zgarish — dev'da (SSE), hamma
+  // sahifada
+  useJonliYangilanish();

vite-env.d.ts da tur: readonly VITE_JONLI_URL?: string; — ? chunki production'da yo'q.

7.4 O'lchov va tekshiruvlar

Dev server va haqiqiy «Bahor» API bilan, ikki brauzer konteksti ("qurilma") — jonli.sh + jonli-dev.mjs (Chrome 155). Natija (natijalar/18-jonli-dev.txt, aynan):

text
Chrome: Chrome/155.0.8059.40 | dev: http://localhost:5730/
✅ SSE ulanishi — shu kalit bilan (ikkala qurilma): [true,true]
   (EventSource so'rovlari: A 1, B 1 — StrictMode)
✅ B: A dagi o'zgarish ko'rindi (sahifa yangilanmasdan): true
   (A bosdi → B ekranida: 254 ms; A so'rovlari: PATCH,GET; B so'rovlari: GET)
✅ A: o'z hodisasini tanidi — PATCH va yozuvdan keyingi bitta GET: ["PATCH","GET"]
✅ B: faqat bitta GET (hodisa → invalidateQueries): ["GET"]
✅ CSP buzilishi yo'q, konsol xatosi yo'q: [[],[],[],[]]
HAMMASI ✅

A bosdi → B ekranida 254 ms. Oldin — 30 soniyagacha. A da faqat PATCH va mutatsiya dan keyingi bitta GET: o'z hodisasiga javob bermadi. B da — bitta GET.

Production uchun alohida tekshiruv (e2e-19.mjs): build'dagi JS fayllarda EventSource so'zi yo'q. Vite import.meta.env.VITE_JONLI_URL ni undefined bilan almashtiradi, if (manzil === undefined) return dan keyingi kod "o'lik" bo'ladi va bundler uni tashlaydi. Build hajmi deyarli o'zgarmadi (gzip 129 822 → 129 666 B).

Testlar: tekshiruv/jonli.test.js — plaginning 3 ta node:test tekshiruvi (soxta so'rov va javob bilan). node:test 174 → 177; «Orqaga» tuzatishidan (React Router III) keyin kanonda — 185. Vitest 56.

bash
git add -A
git commit

Commit (kanon): feat: dev server'da real-time (SSE) — boshqa qurilmadagi o'zgarish. Diff: 10 fayl, +223 −1.

Production'da real-time — Backend integratsiyalar qismida, o'z serverimiz bilan. Klient kodi (jonli.ts) o'zgarmaydi: faqat VITE_JONLI_URL production'da ham bo'ladi.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
har xabar ikki marta keladi (dev) cleanup yo'q, StrictMode ikkita ulanish qoldirdi return () => socket.close()
sahifadan chiqqach ham xabarlar keladi cleanup yo'q o'sha
ro'yxatda faqat oxirgi xabar ishlovchida eskirgan state updater: set((list) => [...list, x])
WebSocket uzildi va qayta ulanmadi WebSocket o'zi ulanmaydi close da eksponensial kutish bilan qayta
o'z yozuvidan keyin ortiqcha GET o'z hodisasi tanilmagan xabarda manba belgisi
Unexpected token … in JSON xabar JSON emas (ping, matn) try/catch yoki xabar turini tekshiring
har komponent o'z ulanishini ochgan ulanish komponentlarda bitta joyda, natija — keshga
polling fon tabda to'xtadi refetchIntervalInBackground: false kerak bo'lsa true

9. Mashqlar

Mashqlar kurs/mashqlar/19/18-real-time/ da.

1-mashq (oson): qaysi usul?

Har vaziyat uchun yozing: "polling", "SSE" yoki "WebSocket".

  • Kuryer ilovasi: mijoz kuryerning xaritadagi joyini ko'radi, kuryer har soniyada koordinata yuboradi, mijoz faqat ko'radi → mijoz tomoni:
  • Ikki o'yinchili shaxmat →
  • Kunlik savdo statistikasi paneli, 1 daqiqalik kechikish mayli →
Yechim
  1. SSE — mijoz faqat tinglaydi; kuryer koordinatani o'z kanali bilan (POST yoki WebSocket) yuboradi, bu mijoz brauzerining ishi emas.
  2. WebSocket — ikkala tomon tez-tez yozadi (yurishlar), kechikish sezilarli.
  3. Polling — refetchInterval: 60_000. Oddiy, maxsus server shart emas.

2-mashq (o'rta): cleanup'ni tuzating

Komponent har «Qayta ochish» bosilganda yangi ulanish ochadi, lekin yopmaydi. Natija — xabarlar ko'payib boradi. Bitta qator qo'shing.

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

let open = 0;

class FakeSocket extends EventTarget {
  constructor() {
    super();
    open += 1;
    this.timer = setInterval(
      () => this.dispatchEvent(new Event("message")),
      100,
    );
  }
  close() {
    open -= 1;
    clearInterval(this.timer);
  }
}

function Kitchen() {
  useEffect(() => {
    const socket = new FakeSocket();
    socket.addEventListener("message", () => {});
  }, []);
  return <p>Oshxona</p>;
}

function Page() {
  const [on, setOn] = useState(true);
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setCount(open), 50);
    return () => clearInterval(id);
  }, []);
  return (
    <>
      <button onClick={() => setOn(false)}>Yopish</button>{" "}
      <button onClick={() => setOn(true)}>Ochish</button>
      {on && <Kitchen />}
      <p>Ochiq ulanishlar: {count}</p>
    </>
  );
}

export default function App() {
  return <Page />;
}

Ekranda:

text
Yopish Ochish
Oshxona
Ochiq ulanishlar: 2
Yechim
jsx
import { useEffect, useState } from "react";

let open = 0;

class FakeSocket extends EventTarget {
  constructor() {
    super();
    open += 1;
    this.timer = setInterval(
      () => this.dispatchEvent(new Event("message")),
      100,
    );
  }
  close() {
    open -= 1;
    clearInterval(this.timer);
  }
}

function Kitchen() {
  useEffect(() => {
    const socket = new FakeSocket();
    socket.addEventListener("message", () => {});
    return () => socket.close();
  }, []);
  return <p>Oshxona</p>;
}

function Page() {
  const [on, setOn] = useState(true);
  const [count, setCount] = useState(0);
  useEffect(() => {
    const id = setInterval(() => setCount(open), 50);
    return () => clearInterval(id);
  }, []);
  return (
    <>
      <button onClick={() => setOn(false)}>Yopish</button>{" "}
      <button onClick={() => setOn(true)}>Ochish</button>
      {on && <Kitchen />}
      <p>Ochiq ulanishlar: {count}</p>
    </>
  );
}

export default function App() {
  return <Page />;
}

Ekranda:

text
Yopish Ochish
Oshxona
Ochiq ulanishlar: 1

return () => socket.close(); — bitta qator. Yopilganda ulanish yopiladi, ochilganda yangisi — doim bitta. Asl kodda ikkinchi ochilishda birinchi ulanish ham tirik edi — 2. Ming marta ochilsa — ming.

3-mashq (qiyin): jonli reyting

Taom reytingi useQuery bilan serverdan keladi (fetchRating → 4.5). Ulanish (FakeSocket) har xabarda yangi reytingni yuboradi ({ rating: 4.6 }). Yozing:

  1. useLiveRating hook'i: effektda ulanish, xabarda setQueryData(["rating"], …), cleanup.
  2. staleTime: Infinity.
  3. Ekranda: "Reyting: 4.7" (oxirgi xabardan keyin).

Soxta ulanish (boshlang'ich kod):

jsx
class FakeSocket extends EventTarget {
  constructor() {
    super();
    const values = [4.6, 4.7];
    this.timer = setInterval(() => {
      const value = values.shift();
      if (value === undefined) return;
      const data = JSON.stringify({ rating: value });
      this.dispatchEvent(new MessageEvent("message", { data }));
    }, 150);
  }
  close() {
    clearInterval(this.timer);
  }
}

Ishora: «Ma'lumot xabarda: setQueryData» bo'limi. Bu safar yangilovchi funksiya shart emas — yangi qiymat eskisiga bog'liq emas.

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

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: Infinity } },
});

class FakeSocket extends EventTarget {
  constructor() {
    super();
    const values = [4.6, 4.7];
    this.timer = setInterval(() => {
      const value = values.shift();
      if (value === undefined) return;
      const data = JSON.stringify({ rating: value });
      this.dispatchEvent(new MessageEvent("message", { data }));
    }, 150);
  }
  close() {
    clearInterval(this.timer);
  }
}

function fetchRating() {
  console.log("GET reyting");
  return Promise.resolve(4.5);
}

function useLiveRating() {
  const client = useQueryClient();
  useEffect(() => {
    const socket = new FakeSocket();
    socket.addEventListener("message", (event) => {
      const { rating } = JSON.parse(event.data);
      client.setQueryData(["rating"], rating);
    });
    return () => socket.close();
  }, [client]);
}

function Rating() {
  useLiveRating();
  const { data } = useQuery({
    queryKey: ["rating"],
    queryFn: fetchRating,
  });
  return <p>Reyting: {data}</p>;
}

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

Konsolda:

text
GET reyting

Ekranda:

text
Reyting: 4.7

Bitta GET (boshlang'ich), keyin ikki xabar keshni almashtirdi. setQueryData(["rating"], rating) — funksiya emas, qiymat: reyting butunligicha keldi. Ro'yxatda esa (buyurtmalar) yangilovchi funksiya kerak edi — yangi element eskilariga qo'shiladi.

4-mashq: Vazifalar qadami

feat/jonli-dev branch'ida: skriptlar/jonli.js, vite.config.ts dagi plagin, .env.development dagi VITE_JONLI_URL, src/jonli.ts, src/jonli-manba.ts, api.ts dagi yozuvSarlavhalari, server-sinxron.ts dagi sarlavha, Qobiq.tsx dagi hook, vite-env.d.ts, tekshiruv/jonli.test.js. Keyin commit.

Yechim

Tekshiruv ro'yxati:

  • npm run dev, ikki xil brauzerda (yoki oddiy va inkognito oynada) bir kalit bilan oching. Bittasida vazifani belgilang — ikkinchisida ~0.3 s da o'zgaradi.
  • DevTools Network: /api/jonli/… so'rovi "pending" holatida turadi (doimiy ulanish), EventStream tabida ozgardi hodisalari.
  • Belgilagan tabda faqat PATCH va bitta GET — o'z hodisasiga javob yo'q.
  • npm run build va dist/assets/*.js da EventSource qidiring — yo'q.
  • npm test — node:test 185 («Orqaga» tuzatishigacha — 177), Vitest 56.

10. Real ishda

  • Chat va hamkorlik (Telegram Web, Google Docs, Figma) — WebSocket. Ko'pincha tayyor xizmatlar: Pusher, Ably, Supabase Realtime, Firebase (Tayyor realtime xizmatlari).
  • Bildirishnomalar, buyurtma holati, AI javoblari — SSE. ChatGPT kabi AI chat'lar javobni so'zma-so'z aynan SSE (yoki stream) bilan yuboradi.
  • Admin panellar — ko'pincha polling: refetchInterval oddiy va ishonchli.
  • Masshtab — 10 000 ta doimiy ulanish server uchun yuk. Server tomonidagi muammolar (bir nechta server orasida xabar tarqatish) — Realtime'ni masshtablash darsida.
  • Intervyu savollari: "WebSocket va SSE farqi?", "React'da WebSocket'ni qayerda ochasiz va qanday yopasiz?", "uzilgan ulanishni qanday tiklaysiz?", "real-time xabarni React Query keshi bilan qanday birlashtirasiz?".

Xulosa

  • Polling (refetchInterval) — oddiy, kechikish = oraliq. SSE — serverdan bir tomonlama, qayta ulanish tayyor. WebSocket — ikki tomonlama. Socket.IO — WebSocket ustida kutubxona, serveri ham Socket.IO.
  • Ulanish — effektda, yopish — cleanup'da. StrictMode'dagi "ochildi-yopildi-ochildi" — cleanup to'g'riligining belgisi.
  • Xabar Query keshiga: to'liq ma'lumot — setQueryData, faqat signal — invalidateQueries. O'z xabaringizni taniting.
  • WebSocket'ni qo'lda qayta ulang: eksponensial kutish + jitter. Ulanish holatini matn bilan ko'rsating.
  • vazifalar: dev server SSE + invalidateQueries — 254 ms; production build'da kod yo'q.

Keyingi dars: GraphQL va tRPC klientlari (tanishuv) — REST'dan boshqa yo'llar: so'ralgan maydonlarni olish va server turini to'g'ridan-to'g'ri ishlatish.

Manbalar

  • MDN: WebSocket, EventSource, Server-sent events — developer.mozilla.org/docs/Web/API/WebSocket, …/EventSource
  • TanStack Query v5: refetchInterval, Query Invalidation — tanstack.com/query/v5/docs/framework/react/reference/useQuery (2026-10, v5.104)
  • TkDodo: Using WebSockets with React Query — tkdodo.eu/blog/using-web-sockets-with-react-query
  • Socket.IO: Client API — socket.io/docs/v4/client-api
  • Vite: Plugin API, configureServer — vite.dev/guide/api-plugin
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React real-time: WebSocket, SSE va Socket.IO bilan jonli ma'lumot — IlmHamroh