Mundarija (33)
- Bu darsda
- 1. Nega bu kerak?
- 2. Tashqi manba nima
- 2.1 React'dan tashqarida yashaydigan qiymatlar
- 2.2 Vanilla store
- 3. Birinchi urinish: useState + useEffect
- 3.1 O'tkazib yuborilgan o'zgarish
- 3.2 Nega shunday bo'ldi
- 4. useSyncExternalStore
- 4.1 Sintaksis
- 4.2 React ichkarida nima qiladi
- 5. Brauzer manbalari
- 5.1 Internet holati
- 5.2 URL
- 6. Ikki qoida
- 6.1 getSnapshot — o'sha qiymat, yangi obyekt emas
- 6.2 subscribe — har renderda yangi funksiya bo'lmasin
- 7. Serverda: getServerSnapshot
- 8. "Tearing" — nega aynan shu hook
- 9. Zustand ichida
- 10. Vazifalar qadami: filtr, saralash va internet — tashqi manbadan
- 10.1 Oldingi holat
- 10.2 URL — tashqi manba (marshrut.ts)
- 10.3 App.tsx
- 10.4 Testlar va natija
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): savatdagi taomlar soni
- 2-mashq (o'rta): cheksiz sikl
- 3-mashq (qiyin): o'z useStore ingiz
- 13. Real ishda
- Xulosa
- Manbalar
React useSyncExternalStore: internet holati, URL va tashqi store'ga to'g'ri obuna
Qisqacha: React'dan tashqarida yashaydigan ma'lumotni (internet bormi, URL, alohida modul-store) o'qish uchun
useSyncExternalStore(subscribe, getSnapshot)ishlatiladi.subscribe— o'zgarishni eshitish va obunani bekor qilish funksiyasi,getSnapshot— joriy qiymatni qaytaradi. React o'zi obuna bo'ladi, qiymatni har renderda tekshiradi va o'zgarsa qayta chizadi.useState+useEffectbilan qo'lda obuna bo'lishdan farqi — o'zgarishni o'tkazib yubormaydi va bir ekranda ikki xil qiymat chiqmaydi. Ikki qoida:getSnapshoto'zgarmagan ma'lumot uchun o'sha qiymatni qaytarsin,subscribeesa har renderda yangi funksiya bo'lmasin.
Bu darsda
- "Tashqi manba" nima ekanini va uni React'ga ulashning to'g'ri yo'lini tushuntira olasiz.
useState+useEffectobunasidagi bo'shliqni (o'tkazib yuborilgan o'zgarish) ko'rsatib berasiz.useSyncExternalStorebilan internet holati, URL va o'z store'ingizni o'qiysiz.getSnapshotvasubscribetuzoqlarini taniysiz,getServerSnapshotnima uchun ekanini bilasiz.- "Tearing" so'zini va Zustand ichida shu hook turganini tushunasiz.
Oldin bilishingiz kerak: useEffect III: cleanup, Sizga effekt kerak emas, JS'da dizayn naqshlari (observer), navigator va qurilma API'lari.
1. Nega bu kerak?
vazifalar da internet uzilsa, tepada "Internet yo'q" yozuvi chiqadi. Internet bormi-yo'qmi — buni brauzer biladi: navigator.onLine va online/offline hodisalari (navigator). React buni bilmaydi. useEffect III darsida biz shunday yozgan edik: useState da nusxa, effektda hodisaga obuna va setState. Filtr bilan ham xuddi shunday: URL'dagi ?filtr=faol — state'da nusxa, popstate effektida yangilash.
Bu ishlaydi, lekin bitta ma'lumot ikki joyda yashaydi: brauzerda va state'da. Ular orasida esa vaqt oralig'i bor — nusxa eskirib qolishi mumkin. Bugun bunday ma'lumot uchun maxsus hook'ni o'rganamiz: useSyncExternalStore — "tashqi do'konga sinxron obuna".
Hayotiy o'xshatish: oshxonadagi buyurtmalar taxtasi. Ofitsiant har safar taxtaga qarab buyurtmani o'qiydi. Agar u taxtadagi narsani daftariga ko'chirib olib, daftardan ishlasa, taxta o'zgarganini bilmay qolishi mumkin. To'g'ri yo'l — taxtaning o'ziga qarash va "taxta o'zgardi" qo'ng'irog'ini eshitib turish.
2. Tashqi manba nima
2.1 React'dan tashqarida yashaydigan qiymatlar
Tashqi manba (external store) — qiymati React state'ida emas, boshqa joyda saqlanadigan va o'zi o'zgarib turadigan narsa:
| Manba | Joriy qiymat | O'zgarish xabari |
|---|---|---|
| internet holati | navigator.onLine |
online, offline hodisalari |
| URL | location.href, location.hash |
popstate, hashchange |
| ekran temasi | matchMedia("(prefers-color-scheme: dark)").matches |
change |
boshqa tab yozgan localStorage |
localStorage.getItem(...) |
storage |
| o'zingiz yozgan modul-store | store.get() |
store.subscribe(...) |
| Zustand, Redux kabi kutubxona | store.getState() |
store.subscribe(...) |
Hammasida bir xil ikki narsa bor: joriy qiymatni olish va o'zgarishga obuna bo'lish. Bu — observer naqshi: kuzatuvchilar ro'yxati, o'zgarishda hammasiga xabar.
2.2 Vanilla store
React'siz oddiy store — 10 qator:
function createStore(initial) {
let value = initial;
const listeners = new Set();
return {
get: () => value,
set(next) {
value = next;
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener); // obunani bekor qilish
},
};
}
const queue = createStore(0);
const stop = queue.subscribe(() => {
console.log("navbat:", queue.get());
});
queue.set(1);
queue.set(2);
stop();
queue.set(3); // endi hech kim eshitmaydi
console.log("oxiri:", queue.get());Konsolda:
navbat: 1
navbat: 2
oxiri: 3subscribe bekor qiluvchi funksiya qaytaradi — xuddi effektning cleanup'i kabi (useEffect III). useSyncExternalStore aynan shu shakldagi API'ni kutadi.
3. Birinchi urinish: useState + useEffect
3.1 O'tkazib yuborilgan o'zgarish
«Bahor» kassasida buyurtmalar navbati — React'dan tashqaridagi store (u yerga server xabarlari ham tushishi mumkin). App navbatdagi sonni ko'rsatadi. Kitchen komponenti ochilishi bilan birinchi buyurtmani qo'shadi. Odatiy usul bilan:
import { useEffect, useState } from "react";
// React'dan tashqaridagi store
const queue = { count: 0, listeners: new Set() };
function subscribe(listener) {
queue.listeners.add(listener);
return () => queue.listeners.delete(listener);
}
function addOrder() {
queue.count++;
queue.listeners.forEach((listener) => listener());
}
function Kitchen() {
useEffect(() => {
addOrder(); // ochilishi bilan buyurtma
}, []);
return <p>Oshxona ishlayapti</p>;
}
function App() {
const [count, setCount] = useState(queue.count); // nusxa
useEffect(() => subscribe(() => setCount(queue.count)), []);
return (
<div>
<p>Navbatda: {count}</p>
<Kitchen />
</div>
);
}
setTimeout(() => console.log("haqiqiy:", queue.count), 50);Konsolda:
haqiqiy: 1Ekranda:
Navbatda: 0
Oshxona ishlayaptiStore'da 1 ta buyurtma, ekranda — 0. Va bu xato hech qachon o'zi tuzalmaydi: keyingi buyurtmagacha App 0 ni ko'rsataveradi.
3.2 Nega shunday bo'ldi
Effektlar bolalardan boshlab ishlaydi: avval Kitchen niki, keyin App niki (Effekt hayot sikli).
| Qadam | queue.count |
App dagi count |
Obuna bormi |
|---|---|---|---|
render: useState(queue.count) |
0 | 0 | yo'q |
Kitchen effekti: addOrder() |
1 | 0 | yo'q — xabarni hech kim eshitmadi |
App effekti: subscribe(...) |
1 | 0 | bor, lekin kech |
| ekran | 1 | 0 | — |
Render va obuna orasida bo'shliq bor. O'sha oraliqda o'zgarish bo'lsa — yo'qoladi. Bu faqat bola effekti emas: server javobi, taymer, boshqa tab — istalgan narsa shu oraliqqa tushishi mumkin. Tuzatish uchun obunadan keyin qiymatni yana bir marta o'qish kerak edi. Buni har safar qo'lda yozish — esdan chiqadigan ish.
Tekshirib ko'ring:
useState(queue.count)o'rnigauseState(0)deb yozsak, natija o'zgaradimi?
Javob
Yo'q — baribir 0. Muammo boshlang'ich qiymatda emas, obunaning kechikishida. App o'zgarishni eshitmagan, demak setCount hech qachon chaqirilmaydi.
4. useSyncExternalStore
4.1 Sintaksis
const value = useSyncExternalStore(
subscribe,
getSnapshot,
getServerSnapshot, // ixtiyoriy
);subscribe(callback)— React bergancallbackni manbaning o'zgarish xabariga ulaydi va bekor qiluvchi funksiya qaytaradi.getSnapshot()— joriy qiymatni qaytaradi ("snapshot" — shu lahzadagi surat).getServerSnapshot()— ixtiyoriy, serverda chizish uchun (pastda).
O'sha navbat, faqat hook almashdi:
import { useEffect, useSyncExternalStore } from "react";
const queue = { count: 0, listeners: new Set() };
function subscribe(listener) {
queue.listeners.add(listener);
return () => queue.listeners.delete(listener);
}
function addOrder() {
queue.count++;
queue.listeners.forEach((listener) => listener());
}
function Kitchen() {
useEffect(() => {
addOrder();
}, []);
return <p>Oshxona ishlayapti</p>;
}
function App() {
const count = useSyncExternalStore(subscribe, () => queue.count);
return (
<div>
<p>Navbatda: {count}</p>
<Kitchen />
</div>
);
}
setTimeout(() => console.log("haqiqiy:", queue.count), 50);Konsolda:
haqiqiy: 1Ekranda:
Navbatda: 1
Oshxona ishlayaptiuseState ham, effekt ham yo'q — App da nusxa saqlanmaydi. Qiymat har doim store'ning o'zidan.
4.2 React ichkarida nima qiladi
sequenceDiagram
participant C as Komponent
participant R as React
participant S as Store
C->>R: useSyncExternalStore(subscribe, getSnapshot)
R->>S: getSnapshot() — render paytida
R->>S: subscribe(callback) — commit'dan keyin
R->>S: getSnapshot() — obunadan keyin yana bir bor
S-->>R: callback() — o'zgardi!
R->>S: getSnapshot()
R->>C: qiymat boshqacha (Object.is) — qayta renderDiagrammadagi uchinchi o'q — bo'shliqni yopadigan qadam. Obunadan keyin React qiymatni qayta o'qiydi. Render va obuna orasida o'zgarish bo'lgan bo'lsa, darhol sezadi va qayta chizadi. Xabar kelganda React yangi qiymatni eskisi bilan Object.is orqali solishtiradi. Teng bo'lsa — qayta chizmaydi.
| Qadam | queue.count |
getSnapshot() |
Ekranda |
|---|---|---|---|
| render | 0 | 0 | — |
Kitchen effekti: addOrder() |
1 | — | — |
| React obuna bo'ldi va qayta o'qidi | 1 | 1 ≠ 0 | qayta render |
| natija | 1 | 1 | Navbatda: 1 |
5. Brauzer manbalari
5.1 Internet holati
navigator.onLine va ikkita hodisa:
import { useSyncExternalStore } from "react";
function subscribe(callback) {
addEventListener("online", callback);
addEventListener("offline", callback);
return () => {
removeEventListener("online", callback);
removeEventListener("offline", callback);
};
}
const getSnapshot = () => navigator.onLine;
// Faqat namuna uchun: brauzerdagi internetni "soxta" uzamiz
function fakeNetwork(online) {
Object.defineProperty(navigator, "onLine", {
value: online,
configurable: true,
});
dispatchEvent(new Event(online ? "online" : "offline"));
}
function App() {
const online = useSyncExternalStore(subscribe, getSnapshot);
console.log("render", online);
return (
<div>
<p>{online ? "Onlayn" : "Internet yo'q"}</p>
<button onClick={() => fakeNetwork(false)}>
Internetni uzish
</button>{" "}
<button onClick={() => fakeNetwork(true)}>Qayta ulash</button>
</div>
);
}Konsolda:
render true
render false
render trueEkranda:
Onlayn
Internetni uzish Qayta ulashfakeNetwork — faqat namuna uchun: u navigator.onLine ni almashtirib, hodisani o'zimiz yuboradi. Haqiqiy sinov — DevTools → Network → "Offline" (DevTools II): brauzer o'zi offline hodisasini beradi.
| Qadam | navigator.onLine |
Ekranda |
|---|---|---|
| boshida | true |
Onlayn |
offline hodisasi |
false |
Internet yo'q |
online hodisasi |
true |
Onlayn |
subscribe va getSnapshot komponentdan tashqarida e'lon qilingan. Bu muhim — «Ikki qoida» bo'limida ko'rasiz.
5.2 URL
URL ham tashqi manba. Oddiylik uchun location.hash ni olamiz (#faol):
import { useSyncExternalStore } from "react";
function subscribe(callback) {
addEventListener("hashchange", callback);
return () => removeEventListener("hashchange", callback);
}
const getFilter = () => location.hash.slice(1) || "hammasi";
const go = (hash) => (location.hash = hash); // URL'ga yozish
function App() {
const filter = useSyncExternalStore(subscribe, getFilter);
return (
<div>
<p>Filtr: {filter}</p>
<button onClick={() => go("faol")}>Faol</button>{" "}
<button onClick={() => go("")}>Hammasi</button>
</div>
);
}Ekranda:
Filtr: faol
Faol HammasiTugma state'ga emas, URL'ga yozadi. Ekran URL'dan o'qiydi. Foydalanuvchi «Orqaga» tugmasini bossa, brauzer URL'ni o'zgartiradi va hashchange beradi — ekran ham o'zi ergashadi. Ma'lumot bitta joyda: URL'da.
vazifalar da filtr hash'da emas, ?filtr=faol qismida, va URL history.pushState bilan yoziladi (History API). Bunda bitta nozik joy bor: brauzer popstate ni faqat «Orqaga/Oldinga» da beradi, pushState da — bermaydi. Shuning uchun kanon kodimiz o'zi yozganda tinglovchilarga o'zi xabar beradi («Vazifalar qadami» da).
Tekshirib ko'ring:
getFilternegalocation.hashni to'g'ridan-to'g'ri emas,.slice(1) || "hammasi"qilib qaytaradi? BugetSnapshotqoidasiga zid emasmi?
Javob
Zid emas. getSnapshot istalgan hisob qila oladi — muhimi, manba o'zgarmagan bo'lsa teng qiymat qaytarsin. Satr va son Object.is da qiymati bo'yicha solishtiriladi: "faol" === "faol". Shuning uchun satr qaytaradigan getSnapshot xavfsiz. Muammo obyekt qaytarilganda boshlanadi — keyingi bo'limda.
6. Ikki qoida
6.1 getSnapshot — o'sha qiymat, yangi obyekt emas
Sardor store'dan obyekt qaytardi:
import { useSyncExternalStore } from "react";
const state = { count: 0 };
const subscribe = () => () => {};
function App() {
// ❌ har chaqiruvda YANGI obyekt
const snap = useSyncExternalStore(subscribe, () => ({ ...state }));
return <p>{snap.count}</p>;
}Konsolda:
The result of getSnapshot should be cached to avoid an infinite loop
Uncaught Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.Birinchi qator — ogohlantirish: "getSnapshot natijasi keshlanishi kerak, aks holda cheksiz sikl." Ikkinchisi — xato: "Ichma-ich yangilanishlar chegarasidan oshdi." Matnda class komponent metodlari (componentDidUpdate) tilga olingan — bu umumiy xabar, sababi bizda boshqa.
Nima bo'ldi: React har renderda va obunadan keyin getSnapshot() ni chaqirib, avvalgisi bilan Object.is qiladi. { ...state } har safar yangi obyekt — "o'zgardi" — qayta render — yana yangi obyekt...
Tuzatish yo'llari:
- Primitiv qaytaring:
() => state.count— son,Object.isqiymatini solishtiradi. - Obyektni store o'zi saqlasin: o'zgarishda yangi obyekt yasang (immutable, State III),
getSnapshotesa doim o'sha havolani qaytarsin. BizningcreateStoredagiget: () => valueaynan shunday. - Hosilaviy massiv (masalan,
filternatijasi) kerak bo'lsa — uni keshlang: manba o'zgarmasa, eski massivni qaytaring (2-mashq).
6.2 subscribe — har renderda yangi funksiya bo'lmasin
import { useState, useSyncExternalStore } from "react";
function App() {
const [clicks, setClicks] = useState(0);
const online = useSyncExternalStore(
(callback) => { // ❌ har renderda yangi funksiya
console.log("obuna");
addEventListener("online", callback);
return () => {
console.log("bekor");
removeEventListener("online", callback);
};
},
() => navigator.onLine,
);
return (
<button onClick={() => setClicks(clicks + 1)}>+1</button>
);
}Konsolda:
obuna
bekor
obuna
bekor
obunaEkranda:
+1Har bosishda App qayta chizildi va subscribe — yangi funksiya. React "obuna funksiyasi almashdi" deb, eskisini bekor qilib, qaytadan obuna bo'ldi. Xato emas, lekin isrof: og'ir obunada (WebSocket, matchMedia) har renderda uzib-ulash sezilarli. Tuzatish — subscribe ni komponentdan tashqariga, modul darajasiga chiqarish («Internet holati» dagidek). Funksiya parametrga bog'liq bo'lsa (masalan, media so'rovi), har parametrga bitta funksiyani saqlab qo'yish kerak — buni Custom hooklar II darsida useMediaQuery da qilamiz.
| Qoida | Buzilsa | To'g'risi |
|---|---|---|
getSnapshot — o'zgarmasa, teng qiymat |
cheksiz sikl, xato | primitiv yoki saqlangan havola |
subscribe — barqaror funksiya |
har renderda qayta obuna | modul darajasida |
subscribe cleanup qaytarsin |
obuna sizadi (memory leak) | return () => remove… |
7. Serverda: getServerSnapshot
Serverda (SSR) navigator ham, location ham yo'q. React serverda getSnapshot o'rniga uchinchi argumentni chaqiradi. U berilmagan bo'lsa — react-dom/server ning renderToString i bilan tekshirdik:
Missing getServerSnapshot, which is required for server-rendered content. Will revert to client rendering."Serverda chizish uchun zarur getServerSnapshot yo'q. Brauzerda chizishga o'tiladi." Ya'ni server bu komponentni chiza olmadi. getServerSnapshot — "serverda qanday qiymat deb hisoblaylik" degan taxmin: internet uchun () => true. Brauzerda hydration'dan keyin React haqiqiy getSnapshot ga o'tadi.
vazifalar da SSR yo'q, lekin kanon useOnlayn uchun uchinchi argumentni baribir yozdi — bu hook qayerda ishlatilishini oldindan bilmaymiz.
8. "Tearing" — nega aynan shu hook
React 18 dan beri React katta renderni bo'laklarga bo'lib, oralarda brauzerga nafas berishi mumkin. Bu concurrent rendering deb ataladi — uni 18-qismda useTransition bilan to'liq o'rganamiz. Hozir bitta oqibatini bilish yetadi.
Tasavvur qiling: ro'yxatda 100 ta qator bor va har biri navbatdagi buyurtmalar sonini tashqi store'dan to'g'ridan-to'g'ri (queue.count) o'qiydi. React 50 ta qatorni chizdi va to'xtab, brauzerga navbat berdi. Shu payt server yangi buyurtma yubordi: count 3 → 4. React davom etib, qolgan 50 tasini chizdi.
| Qatorlar | O'qilgan payt | Ko'rsatgan son |
|---|---|---|
| 1–50 | to'xtashdan oldin | 3 |
| 51–100 | to'xtashdan keyin | 4 |
Bitta ekranda bitta ma'lumot ikki xil ko'rinadi. Bunga tearing ("yirtilish") deyiladi — kadr o'rtasidan yirtilgandek. React'ning o'z state'ida bu bo'lmaydi: React render davomida o'z state'ining bitta "suratini" ishlatadi. Tashqi store esa React'ning nazoratida emas.
useSyncExternalStore buni hal qiladi: React render davomida getSnapshot ni tekshiradi. Qiymat o'zgarganini sezsa, bo'laklarga bo'lishdan voz kechadi va hammasini bitta sinxron (to'xtamasdan) renderda qayta chizadi. Hook nomidagi "Sync" shundan. Tashqi manbani useEffect + useState bilan yoki to'g'ridan-to'g'ri o'qiganda bu kafolat yo'q.
9. Zustand ichida
Zustand — React uchun mashhur kichik state kutubxonasi (Zustand I da o'rganamiz). Uning React qismi — deyarli bitta useSyncExternalStore chaqiruvi. zustand 5.0.15 paketining esm/react.mjs faylidan (npm, 2026-10-13):
function useStore(api, selector = identity) {
const slice = React.useSyncExternalStore(
api.subscribe,
React.useCallback(() => selector(api.getState()), [api, selector]),
React.useCallback(() => selector(api.getInitialState()), [api, selector])
);
React.useDebugValue(slice);
return slice;
}Tanish narsalar: api.subscribe — store'ning obunasi (barqaror, store bilan birga yaratilgan); ikkinchi argument — getSnapshot; uchinchisi — getServerSnapshot (serverda boshlang'ich holat). selector store'ning faqat kerakli qismini oladi — masalan, (s) => s.count. Komponent faqat o'sha qism o'zgarganda qayta chiziladi, chunki Object.is faqat shu qismni solishtiradi. useCallback — funksiyani renderlar orasida saqlab turadigan hook (memo, useMemo, useCallback). useDebugValue — React DevTools'da hook yonida qiymat ko'rsatadi (Custom hooklar II).
Ya'ni kutubxonalar sehrli emas: ular bugungi hook ustiga qurilgan. 3-mashqda shunday useStore ning kichik nusxasini o'zingiz yozasiz.
10. Vazifalar qadami: filtr, saralash va internet — tashqi manbadan
10.1 Oldingi holat
useEffect III darsidan beri App da shunday edi:
// Funksiya — URL faqat birinchi renderda o'qiladi
const [filtr, setFiltr] = useState<Filtr>(() => filtrniOl());
const [saralash, setSaralash] = useState<Saralash>(() =>
saralashniOl(),
);
// … va effektda: popstate → setFiltr(filtrniOl()),
// setSaralash(saralashniOl())
// … oflayn bo'lsa — setTarmoq("oflayn")Bitta ma'lumot — ikki joyda: URL va state. Har tugmada ikki qadam: setFiltr(yangi) va filtrniYoz(yangi). Birini unutish — ekran va URL ajraladi. Yangi branch: refactor/tashqi-manba.
10.2 URL — tashqi manba (marshrut.ts)
// URL — React'dan tashqaridagi manba (17/#38): App unga
// useSyncExternalStore bilan obuna bo'ladi. Brauzer "popstate" ni
// faqat Orqaga/Oldinga'da beradi, pushState'da — yo'q: o'zimiz
// yozganda tinglovchilarga o'zimiz xabar beramiz
const tinglovchilar = new Set<() => void>();
/** @returns obunani bekor qiladi */
export function urlgaObuna(tinglovchi: () => void): () => void {
tinglovchilar.add(tinglovchi);
addEventListener("popstate", tinglovchi);
return () => {
tinglovchilar.delete(tinglovchi);
removeEventListener("popstate", tinglovchi);
};
}Va URL'ga yozadigan funksiyaga bitta qator:
if (url.href !== location.href) {
history.pushState(null, "", url);
+ tinglovchilar.forEach((tinglovchi) => tinglovchi());
}Izohdagi birinchi qator telefonga sig'ishi uchun qayta bo'lindi. urlgaObuna — modul darajasida, barqaror (ikkinchi qoida). URL o'zgarmasa (url.href === location.href) — xabar ham, tarix yozuvi ham yo'q.
10.3 App.tsx
- const [filtr, setFiltr] = useState<Filtr>(() => filtrniOl());
- const [saralash, setSaralash] = useState<Saralash>(() =>
+ const filtr = useSyncExternalStore(urlgaObuna, () => filtrniOl());
+ const saralash = useSyncExternalStore(urlgaObuna, () =>
saralashniOl(),
);filtrniOl() satr qaytaradi ("faol") — birinchi qoida o'z-o'zidan bajariladi. Endi setFiltr yo'q: filtrniTanla faqat filtrniYoz(yangi) qiladi, ekran URL'dan o'qiydi. To'rt joydan setFiltr(...) va setSaralash(...) o'chdi.
Internet uchun — modul darajasida uchta funksiya:
// Internet bormi — brauzer biladi (navigator.onLine), React emas:
// tashqi manba (17/#38). subscribe — o'zgarishni eshitish,
// cleanup qaytaradi
function onlaynObuna(tinglovchi: () => void): () => void {
addEventListener("online", tinglovchi);
addEventListener("offline", tinglovchi);
return () => {
removeEventListener("online", tinglovchi);
removeEventListener("offline", tinglovchi);
};
}
const onlaynmi = () => navigator.onLine;
const serverdaOnlayn = () => true; // SSR'da (bizda yo'q) — taxminKomponent ichida:
// Internet yo'q bo'lsa — server holati qanday bo'lmasin, "oflayn"
const onlayn = useSyncExternalStore(
onlaynObuna,
onlaynmi,
serverdaOnlayn,
);
const aloqa: TarmoqHolati = onlayn ? tarmoq : "oflayn";aloqa — hosilaviy qiymat (State strukturasi): server holati (tarmoq state'i) va internet holatidan har renderda hisoblanadi. Avval offline effekti setTarmoq("oflayn") qilardi — endi kerak emas. offline effekti qoldi, lekin faqat amal uchun: yo'ldagi so'rovni to'xtatish (sinxronniTox). Ekranda nima ko'rinishi — hook'dan.
popstate effekti ham qisqardi — faqat tahrirni yopish qoldi:
useEffect(() => {
- function orqaga() {
- setFiltr(filtrniOl());
- setSaralash(saralashniOl());
- setTahrirId(null);
- }
+ const orqaga = () => setTahrirId(null);
addEventListener("popstate", orqaga);10.4 Testlar va natija
Ikkita yangi test. marshrut.test.js (node:test) — o'zimiz yozsak xabar keladi, URL o'zgarmasa yoki obuna bekor qilinsa — kelmaydi:
test("o'zimiz yozsak — xabar; URL o'zgarmasa yoki bekor qilinsa — yo'q", () => {
…
const bekorQil = urlgaObuna(() => xabarlar++);
filtrniYoz("faol");
filtrniYoz("faol");
assert.equal(xabarlar, 1);
bekorQil();
filtrniYoz("bajarilgan");
assert.equal(xabarlar, 1);
});App.test.tsx (Vitest) — internet uzilsa, ekran darhol o'zgaradi:
test("useSyncExternalStore: internet uzilsa — darhol «oflayn»", async () => {
…
vi.spyOn(navigator, "onLine", "get").mockReturnValue(false);
act(() => {
dispatchEvent(new Event("offline"));
});
expect(matni("aloqa")).toBe(
"Internet yo'q. Ro'yxat — shu brauzerdagi nusxa.",
);
});vi.spyOn(navigator, "onLine", "get") — darsdagi fakeNetwork ning test varianti: getter'ni soxta qiymat bilan almashtiradi. act — React yangilanishini test ichida oxirigacha bajarib oladi.
Haqiqiy tekshiruv natijasi (npm run test):
ℹ tests 149
ℹ pass 149
ℹ fail 0
Test Files 4 passed (4)
Tests 24 passed (24)node:test — 149 (+1), Vitest — 24 (+1). Lint, format, tsc -b — toza. Brauzer tekshiruvi: 159/159 — filtr tugmalari, «Orqaga» tugmasi va oflayn rejim avvalgidek. Diff: 4 fayl, +90 −24.
Commit:
refactor: filtr, saralash va internet — useSyncExternalStore11. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| ekranda eski qiymat, o'zi tuzalmaydi | useState + useEffect obunasi, bo'shliqda o'zgarish |
useSyncExternalStore |
The result of getSnapshot should be cached… + Maximum update depth exceeded |
getSnapshot har safar yangi obyekt/massiv |
primitiv, saqlangan havola yoki kesh |
| har renderda "obuna/bekor" | subscribe komponent ichida |
modul darajasiga chiqaring |
Missing getServerSnapshot… |
SSR'da uchinchi argument yo'q | getServerSnapshot bering |
| filtr tugmasi bosildi, ekran o'zgarmadi | pushState popstate bermaydi |
yozganda tinglovchilarga o'zingiz xabar bering |
| obuna sizadi | subscribe cleanup qaytarmaydi |
return () => remove… |
12. Mashqlar
Mashqlar kurs/mashqlar/17/38-tashqi-manba/ da.
1-mashq (oson): savatdagi taomlar soni
Savat — React'dan tashqaridagi store. Sarlavhadagi belgi taomlar sonini ko'rsatsin. Bo'sh joylarni to'ldiring:
function CartBadge() {
const count = useSyncExternalStore(
cart.[___:subscribe],
() => cart.get().[___:length],
);
return <b>Savatda: {count}</b>;
}Yechim
import { useSyncExternalStore } from "react";
function createStore(initial) {
let value = initial;
const listeners = new Set();
return {
get: () => value,
set(next) {
value = next;
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}
const cart = createStore([]);
const add = (dish) => cart.set([...cart.get(), dish]);
function CartBadge() {
const count = useSyncExternalStore(
cart.subscribe,
() => cart.get().length,
);
return <b>Savatda: {count}</b>;
}
function App() {
return (
<div>
<p><CartBadge /></p>
<button onClick={() => add("Osh")}>Osh qo'shish</button>{" "}
<button onClick={() => add("Manti")}>Manti qo'shish</button>
</div>
);
}Ekranda:
Savatda: 2
Osh qo'shish Manti qo'shishcart.subscribe — store bilan birga bir marta yaratilgan, barqaror. getSnapshot son qaytaradi. set har safar yangi massiv yozadi — eski massiv o'zgarmaydi.
2-mashq (o'rta): cheksiz sikl
Oshxona ekrani faqat tayyor bo'lmagan buyurtmalarni ko'rsatadi. Kod yiqiladi. Sababini toping va tuzating: getSnapshot filtrlangan massivni keshlasin — buyurtmalar massivi o'zgarmagan bo'lsa, oldingi natijani qaytarsin.
import { useSyncExternalStore } from "react";
let orders = [
{ id: 1, dish: "Osh", ready: true },
{ id: 2, dish: "Lag'mon", ready: false },
];
const subscribe = () => () => {};
const getPending = () => orders.filter((o) => !o.ready);
function App() {
const pending = useSyncExternalStore(subscribe, getPending);
return <p>Kutilmoqda: {pending.map((o) => o.dish).join(", ")}</p>;
}Ishora: ikkita modul o'zgaruvchisi — oxirgi ko'rilgan orders va oxirgi natija. orders === lastOrders bo'lsa, lastResult ni qaytaring.
Yechim
import { useSyncExternalStore } from "react";
let orders = [
{ id: 1, dish: "Osh", ready: true },
{ id: 2, dish: "Lag'mon", ready: false },
];
const subscribe = () => () => {};
let lastOrders = null;
let lastResult = [];
function getPending() {
if (orders !== lastOrders) {
lastOrders = orders;
lastResult = orders.filter((o) => !o.ready);
}
return lastResult; // o'zgarmasa — o'sha massiv
}
function App() {
const pending = useSyncExternalStore(subscribe, getPending);
return <p>Kutilmoqda: {pending.map((o) => o.dish).join(", ")}</p>;
}
console.log(getPending() === getPending());Konsolda:
trueEkranda:
Kutilmoqda: Lag'monfilter har chaqiruvda yangi massiv yasardi — React uni "o'zgardi" deb ko'rib, cheksiz qayta chizardi. Endi orders havolasi o'zgarmaguncha natija o'sha. Bu ishlaydi, chunki orders immutable yangilanadi: yangi buyurtma — yangi massiv.
3-mashq (qiyin): o'z useStore ingiz
Zustand'ning kichik nusxasini TypeScript'da yozing:
createStore<T>(initial: T)—getState(),setState(updater: (s: T) => T),subscribe(listener);useStore<T, S>(store, selector: (s: T) => S): S—useSyncExternalStorebilan;- «Bahor» kassasi: holat
{ orders: number; tables: number }. Ikki komponent —OrdersView(faqatordersni o'qiydi) vaTablesView(faqattables). Har biri render bo'lganda konsolga o'z nomini yozsin.
«Buyurtma +1» bosilganda faqat OrdersView qayta chizilishini konsolda ko'rsating. Ishora: selektor son qaytaradi — Object.is sonlarni qiymati bo'yicha solishtiradi. Komponentlar App ning bolasi bo'lmasin-u, App qayta chizilmasin: tugma store'ni o'zgartiradi, App state'ini emas.
Yechim
import { useSyncExternalStore } from "react";
type Store<T> = {
getState: () => T;
setState: (updater: (s: T) => T) => void;
subscribe: (listener: () => void) => () => void;
};
export function createStore<T>(initial: T): Store<T> {
let state = initial;
const listeners = new Set<() => void>();
return {
getState: () => state,
setState(updater) {
state = updater(state);
listeners.forEach((listener) => listener());
},
subscribe(listener) {
listeners.add(listener);
return () => {
listeners.delete(listener);
};
},
};
}
export function useStore<T, S>(
store: Store<T>,
selector: (s: T) => S,
): S {
return useSyncExternalStore(store.subscribe, () =>
selector(store.getState()),
);
}
type Cashier = { orders: number; tables: number };
const cashier = createStore<Cashier>({ orders: 0, tables: 12 });
function OrdersView() {
const orders = useStore(cashier, (s) => s.orders);
console.log("OrdersView");
return <p>Buyurtmalar: {orders}</p>;
}
function TablesView() {
const tables = useStore(cashier, (s) => s.tables);
console.log("TablesView");
return <p>Bo'sh stollar: {tables}</p>;
}
export default function App() {
const addOrder = () =>
cashier.setState((s) => ({ ...s, orders: s.orders + 1 }));
return (
<div>
<OrdersView />
<TablesView />
<button onClick={addOrder}>Buyurtma +1</button>
</div>
);
}Konsolda:
OrdersView
TablesView
OrdersViewEkranda:
Buyurtmalar: 1
Bo'sh stollar: 12
Buyurtma +1Bosilgandan keyin faqat OrdersView qayta chizildi. TablesView ham xabar oldi, lekin uning selektori o'sha 12 ni qaytardi — Object.is(12, 12), React uni o'tkazib yubordi. Selektor yangi obyekt qaytarsa (masalan, (s) => ({ t: s.tables })), 2-mashqdagi cheksiz sikl qaytadi — Zustand'da buning uchun useShallow bor (Zustand I).
13. Real ishda
- State kutubxonalari: Zustand, Redux (
react-redux), Jotai, Valtio — hammasining React qismiuseSyncExternalStoreustida. Ularni 19-qismda o'rganamiz. - Brauzer API'lari: internet holati, ekran o'lchami,
matchMedia(qorong'i tema,prefers-reduced-motion), URL — tayyor hook kutubxonalaridagiuseMediaQuery,useNetworkStatekabi hooklar shu hook ustida (Custom hooklar II). - Eski kodda
useEffect+useStatebilan qo'lda obuna ko'p uchraydi. Kod ko'rigida "bu tashqi manba —useSyncExternalStoreishlating" degan izoh tez-tez beriladi. - Intervyu savollari: "
useSyncExternalStorenima uchun kerak?", "Tearing nima?", "NegagetSnapshotyangi obyekt qaytarmasligi kerak?", "Zustand qanday qilib faqat kerakli komponentni qayta chizadi?".
Xulosa
- Tashqi manba — React'dan tashqarida yashaydigan, o'zi o'zgaradigan qiymat: internet, URL,
matchMedia, store. useState+useEffectobunasida render va obuna orasida bo'shliq bor — o'zgarish yo'qolishi mumkin.useSyncExternalStore(subscribe, getSnapshot)obunadan keyin qiymatni qayta o'qiydi va o'zgarishniObject.isbilan sezadi.getSnapshot— o'zgarmasa teng qiymat (aks holda cheksiz sikl);subscribe— barqaror va cleanup qaytaradi.- SSR uchun —
getServerSnapshot; concurrent rendering'da tearing'dan himoya — shu hookning asosiy sababi. vazifalarda filtr, saralash va internet holati endi state nusxasisiz — URL va brauzerdan.
Keyingi dars: useReducer: murakkab state mantiqini bir joyga yig'ish — vazifalar ning qo'shish, o'chirish, tahrirlash amallarini bitta sof funksiyaga yig'amiz.
Manbalar
- React:
useSyncExternalStore— react.dev/reference/react/useSyncExternalStore - React 18 ishchi guruhi: "Concurrent React for Library Maintainers" va tearing muhokamasi — github.com/reactwg/react-18
- Zustand 5.0.15:
esm/react.mjs(npm paketi, 2026-10-13) — github.com/pmndrs/zustand - MDN:
Navigator.onLine,Window: hashchange event— developer.mozilla.org
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!