IlmHamroh
JavaScript Full-stack/17-qism. React asoslari38/45-dars21 daqiqa
Mundarija (33)

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 + useEffect bilan qo'lda obuna bo'lishdan farqi — o'zgarishni o'tkazib yubormaydi va bir ekranda ikki xil qiymat chiqmaydi. Ikki qoida: getSnapshot o'zgarmagan ma'lumot uchun o'sha qiymatni qaytarsin, subscribe esa 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 + useEffect obunasidagi bo'shliqni (o'tkazib yuborilgan o'zgarish) ko'rsatib berasiz.
  • useSyncExternalStore bilan internet holati, URL va o'z store'ingizni o'qiysiz.
  • getSnapshot va subscribe tuzoqlarini taniysiz, getServerSnapshot nima 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:

js
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:

text
navbat: 1
navbat: 2
oxiri: 3

subscribe 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:

jsx
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:

text
haqiqiy: 1

Ekranda:

text
Navbatda: 0
Oshxona ishlayapti

Store'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'rniga useState(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

jsx
const value = useSyncExternalStore(
  subscribe,
  getSnapshot,
  getServerSnapshot, // ixtiyoriy
);
  • subscribe(callback) — React bergan callback ni 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:

jsx
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:

text
haqiqiy: 1

Ekranda:

text
Navbatda: 1
Oshxona ishlayapti

useState 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 render

Diagrammadagi 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:

jsx
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:

text
render true
render false
render true

Ekranda:

text
Onlayn
Internetni uzish Qayta ulash

fakeNetwork — 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):

jsx
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:

text
Filtr: faol
Faol Hammasi

Tugma 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: getFilter nega location.hash ni to'g'ridan-to'g'ri emas, .slice(1) || "hammasi" qilib qaytaradi? Bu getSnapshot qoidasiga 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:

jsx
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:

text
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.is qiymatini solishtiradi.
  • Obyektni store o'zi saqlasin: o'zgarishda yangi obyekt yasang (immutable, State III), getSnapshot esa doim o'sha havolani qaytarsin. Bizning createStore dagi get: () => value aynan shunday.
  • Hosilaviy massiv (masalan, filter natijasi) kerak bo'lsa — uni keshlang: manba o'zgarmasa, eski massivni qaytaring (2-mashq).

6.2 subscribe — har renderda yangi funksiya bo'lmasin

jsx
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:

text
obuna
bekor
obuna
bekor
obuna

Ekranda:

text
+1

Har 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:

text
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):

js
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:

tsx
// 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)

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:

diff
   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

diff
-  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:

tsx
// 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) — taxmin

Komponent ichida:

tsx
  // 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:

diff
   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:

js
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:

tsx
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):

text
ℹ 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:

text
refactor: filtr, saralash va internet — useSyncExternalStore

11. 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:

jsx
function CartBadge() {
  const count = useSyncExternalStore(
    cart.[___:subscribe],
    () => cart.get().[___:length],
  );
  return <b>Savatda: {count}</b>;
}
Yechim
jsx
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:

text
Savatda: 2
Osh qo'shish Manti qo'shish

cart.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.

jsx
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
jsx
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:

text
true

Ekranda:

text
Kutilmoqda: Lag'mon

filter 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 — useSyncExternalStore bilan;
  • «Bahor» kassasi: holat { orders: number; tables: number }. Ikki komponent — OrdersView (faqat orders ni o'qiydi) va TablesView (faqat tables). 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
tsx
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:

text
OrdersView
TablesView
OrdersView

Ekranda:

text
Buyurtmalar: 1
Bo'sh stollar: 12
Buyurtma +1

Bosilgandan 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 qismi useSyncExternalStore ustida. Ularni 19-qismda o'rganamiz.
  • Brauzer API'lari: internet holati, ekran o'lchami, matchMedia (qorong'i tema, prefers-reduced-motion), URL — tayyor hook kutubxonalaridagi useMediaQuery, useNetworkState kabi hooklar shu hook ustida (Custom hooklar II).
  • Eski kodda useEffect + useState bilan qo'lda obuna ko'p uchraydi. Kod ko'rigida "bu tashqi manba — useSyncExternalStore ishlating" degan izoh tez-tez beriladi.
  • Intervyu savollari: "useSyncExternalStore nima uchun kerak?", "Tearing nima?", "Nega getSnapshot yangi 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 + useEffect obunasida render va obuna orasida bo'shliq bor — o'zgarish yo'qolishi mumkin.
  • useSyncExternalStore(subscribe, getSnapshot) obunadan keyin qiymatni qayta o'qiydi va o'zgarishni Object.is bilan 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.
  • vazifalar da 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
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React useSyncExternalStore: internet holati, URL va tashqi store'ga to'g'ri obuna — IlmHamroh