Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Uch usul: polling, SSE, WebSocket
- 2.1 Polling — refetchInterval
- 2.2 SSE va WebSocket
- 3. Ulanish — effekt
- 3.1 Ochish va yopish
- 3.2 StrictMode: ikki marta ulanish
- 4. Xabar — Query keshiga
- 4.1 Ma'lumot xabarda: setQueryData
- 4.2 Faqat signal: invalidateQueries
- 5. Ulanish uziladi: qayta ulanish va holat
- 5.1 Qayta ulanish
- 5.2 Ulanish holati — foydalanuvchiga
- 6. Haqiqiy API'lar: kod qanday ko'rinadi
- 7. Vazifalar qadami: boshqa qurilmadagi o'zgarish — SSE (holat 18)
- 7.1 Muammo va cheklov
- 7.2 Server tomoni: Vite plagini (skriptlar/jonli.js)
- 7.3 Brauzer tomoni: src/jonli.ts
- 7.4 O'lchov va tekshiruvlar
- 8. Ko'p uchraydigan xatolar
- 9. Mashqlar
- 1-mashq (oson): qaysi usul?
- 2-mashq (o'rta): cleanup'ni tuzating
- 3-mashq (qiyin): jonli reyting
- 4-mashq: Vazifalar qadami
- 10. Real ishda
- Xulosa
- Manbalar
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 ulanishuseEffectda 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:
setQueryDatavainvalidateQueries. - Qayta ulanish (eksponensial kutish) va ulanish holati indikatorini yozasiz.
vazifalarga 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):
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:
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:
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:
ulanmoqda: wss://example.com/oshxona
uzildiEkranda:
UzishUch 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. Undaordersto'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:
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:
ulanmoqda: wss://example.com/oshxona
yopildi
ulanmoqda: wss://example.com/oshxonaOchildi, 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:
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:
GET buyurtmalarEkranda:
#100 — Osh
#101 — Manti
#102 — MantiBitta 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:
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:
GET
signal keldi
GETEkranda:
Buyurtmalar: Osh, Lag'monsetQueryData |
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.
vazifalarda bu —X-Jonli-Manbasarlavhasi.
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. Serverretry: 2000yuborsa, uzilgandan 2 s keyin qayta urinadi.- WebSocket — hech kim.
closehodisasini 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:
// 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:
1 500 650
2 1000 1300
3 2000 2600
4 4000 5200
5 8000 10400
6 16000 20800
7 30000 39000
8 30000 39000Har 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:
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:
🟢 JonliOraliq 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:
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):
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:
// 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.
vazifalarda ulanishQobiqda — 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.
git switch -c feat/jonli-dev7.2 Server tomoni: Vite plagini (skriptlar/jonli.js)
Kanon (izohlar qisqartirilgan qatorlarga bo'lingan):
// 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 faqatnpm run devda.npm run builduni 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 < 400bo'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):
// 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):
// 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):
// 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):
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):
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.
git add -A
git commitCommit (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
- SSE — mijoz faqat tinglaydi; kuryer koordinatani o'z kanali bilan (POST yoki WebSocket) yuboradi, bu mijoz brauzerining ishi emas.
- WebSocket — ikkala tomon tez-tez yozadi (yurishlar), kechikish sezilarli.
- 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.
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:
Yopish Ochish
Oshxona
Ochiq ulanishlar: 2Yechim
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:
Yopish Ochish
Oshxona
Ochiq ulanishlar: 1return () => 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:
useLiveRatinghook'i: effektda ulanish, xabardasetQueryData(["rating"], …), cleanup.staleTime: Infinity.- Ekranda: "Reyting: 4.7" (oxirgi xabardan keyin).
Soxta ulanish (boshlang'ich kod):
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
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:
GET reytingEkranda:
Reyting: 4.7Bitta 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 tabidaozgardihodisalari. - Belgilagan tabda faqat PATCH va bitta GET — o'z hodisasiga javob yo'q.
npm run buildvadist/assets/*.jsdaEventSourceqidiring — 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:
refetchIntervaloddiy 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
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!