IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar26/36-dars21 daqiqa
Mundarija (22)

Jotai, Valtio va MobX: atom va proxy state React'da, signals g'oyasi

Qisqacha: Zustand va Redux'da holat — bitta katta store, komponent selektor bilan bo'lak oladi. Atom modelida (Jotai) aksincha: holat mayda mustaqil bo'laklardan (atomlardan) yig'iladi, hosila atom boshqalaridan hisoblanadi, komponent faqat o'qigan atomlari o'zgarganda chiziladi. Proxy modelida (Valtio, MobX) holatni oddiy JS obyekti kabi to'g'ridan-to'g'ri o'zgartirasiz — cart.items.push(x) — kutubxona Proxy orqali buni sezadi va faqat shu maydonni o'qigan komponentlarni chizadi. Uchalasining umumiy ildizi — signals g'oyasi: qiymat kim uni o'qiganini o'zi biladi. Kanon tajribasida (bir xil kichik vazifa, useReducer + Context variantiga nisbatan): Valtio +1.7 KB, Jotai +2.9, MobX +12.6 — Zustand +0.4.

Bu darsda

  • Atom modelini kutubxonasiz modelda jonli ko'rasiz, keyin Jotai'da atom, hosila atom, yozuvchi atom va atomWithStorage ni ishlatasiz.
  • Valtio'da proxy, useSnapshot va subscribe bilan "o'zgartirib yoz" uslubini sinaysiz.
  • MobX'da makeAutoObservable, computed va autorun nimani avtomatik kuzatishini ko'rasiz.
  • Signals g'oyasini 20 qatorli modelda tushunasiz.
  • To'rt modelni (store, atom, proxy, holat mashinasi) qachon tanlashni o'lchov bilan bilasiz.

Oldin bilishingiz kerak: Zustand I, Redux Toolkit I, Proxy va Reflect, useSyncExternalStore.

1. Nega bu kerak?

«Bahor» saytiga "taom konstruktori" kerak: mehmon osh tanlaydi, porsiya sonini, qo'shimchalarni (qazi, bedana tuxumi), tilni belgilaydi. Ekranda narx, kaloriya, "sizning buyurtmangiz" matni — hammasi bir-biridan hisoblanadi. Har tanlov o'z kichik komponentida.

Sardor buni Zustand'da yozdi: bitta store, o'nta maydon, o'nta selektor. Ishladi. Lekin Jasur aka so'radi: "Ertaga konstruktor uchta sahifada bo'ladi, har birida boshqa taom. Uchta store yasaysizmi?" Malika (dizayner, ekran o'quvchidan foydalanadi) esa boshqa narsani payqadi: forma maydonlari ko'p, har harfda butun konstruktor qayta chizilyapti.

Bunday "ko'p mayda, bir-biriga bog'liq qiymatlar" vazifasida boshqa modellar qulayroq bo'lishi mumkin. Hayotiy o'xshatish: Excel jadvali. A1 — narx, A2 — porsiya, A3 = A1 × A2. A1 ni o'zgartirsangiz, faqat A3 qayta hisoblanadi — butun jadval emas. Har katak — atom, formula — hosila atom. Jadvalni "kimdir A1 ni o'qiyaptimi?" deb kuzatish — signals g'oyasi.

2. Atom modeli

2.1 Jonli model: atom 15 qatorda

Jotai saytdagi natija oynasida yo'q. G'oyani kutubxonasiz o'quv modeli bilan ko'ramiz (bu Jotai emas). Atom — o'z qiymati va obunachilari bor eng kichik holat birligi:

jsx
import { useSyncExternalStore } from "react";

// O'quv modeli: atom (Jotai emas!)
function atom(initial) {
  let value = initial;
  const listeners = new Set();
  return {
    get: () => value,
    set(next) {
      value = typeof next === "function" ? next(value) : next;
      listeners.forEach((listener) => listener());
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

function useAtom(a) {
  return [useSyncExternalStore(a.subscribe, a.get), a.set];
}

const portionsAtom = atom(1);
const langAtom = atom("uz");

function Portions() {
  const [portions, setPortions] = useAtom(portionsAtom);
  console.log("Portions chizildi:", portions);
  return (
    <>
      <button onClick={() => setPortions((n) => n + 1)}>
        +1 porsiya
      </button>{" "}
      ({portions})
    </>
  );
}

function Lang() {
  const [lang, setLang] = useAtom(langAtom);
  console.log("Lang chizildi:", lang);
  return <button onClick={() => setLang("en")}>Til</button>;
}

export default function App() {
  return (
    <p>
      <Portions /> <Lang />
    </p>
  );
}

Konsolda:

text
Portions chizildi: 1
Lang chizildi: uz
Portions chizildi: 2
Lang chizildi: en

Har komponent faqat o'z atomiga obuna. Porsiya o'zgarganda Lang chizilmadi, til o'zgarganda — Portions. Selektor yozmadik: atomning o'zi "bo'lak". Zustand'da bitta store va selektorlar bilan erishgan natijamizga bu yerda tuzilmaning o'zi bilan erishdik.

2.2 Jotai: atom, hosila va yozuvchi atom

Jotai (yaponcha "holat") — atom modelidagi React kutubxonasi, Zustand mualliflari jamoasidan (pmndrs). Kursda versiyasi 3.0.1 (kanon tajribasi). React'siz qismini — jotai/vanilla — Node'da sinaymiz:

js
import { atom, createStore } from "jotai/vanilla";

const priceAtom = atom(35000);
const portionsAtom = atom(1);
// hosila atom: boshqa atomlardan hisoblanadi
const totalAtom = atom((get) => get(priceAtom) * get(portionsAtom));
// faqat yozish uchun atom (amal)
const addPortionAtom = atom(null, (get, set) =>
  set(portionsAtom, get(portionsAtom) + 1),
);

const store = createStore();
store.sub(totalAtom, () =>
  console.log("jami:", store.get(totalAtom)),
);

console.log("boshida:", store.get(totalAtom));
store.set(addPortionAtom);
store.set(addPortionAtom);
store.set(priceAtom, 38000);
store.set(priceAtom, 38000);

Toza papkada jotai@3.0.1 bilan Node 24 da:

text
boshida: 35000
jami: 70000
jami: 105000
jami: 114000

Uch xil atom:

  • atom(35000) — oddiy atom: boshlang'ich qiymat.
  • atom((get) => …) — hosila atom (derived atom): o'qish funksiyasi. get(priceAtom) — "men shu atomga bog'liqman" degan e'lon ham. Narx yoki porsiya o'zgarsa — qayta hisoblanadi, boshqa hech narsa o'zgarsa — yo'q. Excel'dagi formula.
  • atom(null, (get, set) => …) — yozuvchi atom: qiymati yo'q (null), faqat amal. Zustand'dagi store amali kabi, lekin u ham atom.

Oxirgi ikki qatorga qarang: narx ikki marta 38 000 ga o'rnatildi, lekin jami: faqat bir marta chiqdi. Qiymat o'zgarmasa (Object.is), obunachilar bezovta qilinmaydi.

Atom — faqat tavsif (konfiguratsiya), qiymat atomda emas, store da saqlanadi: store.get(atom). React'da store'ni <Provider> beradi yoki u bo'lmasa — umumiy standart store. Bu Zustand II dagi "har so'rovga alohida store" muammosini o'zi yechadi: Next.js'da <Provider> har so'rovga yangi store yaratadi, atomlar esa modul darajasida qolaveradi.

React'da hooklar useState ga o'xshaydi:

jsx
import { useAtom, useAtomValue, useSetAtom } from "jotai";

// o'qish va yozish
const [portions, setPortions] = useAtom(portionsAtom);
const total = useAtomValue(totalAtom); // faqat o'qish
const addPortion = useSetAtom(addPortionAtom); // faqat yozish

useSetAtom — o'tgan darslardagi "faqat amal oladigan komponent chizilmaydi" qoidasi: atom qiymati o'zgarsa ham bu komponent chizilmaydi.

2.3 atomWithStorage — saqlanadigan atom

jotai/utils dagi atomWithStorage(kalit, boshlang'ich) — localStorage ga yoziladigan atom (Zustand II dagi persist ning atom varianti). Quyidagi ilovani React 19.3.0 va jotai 3.0.1 bilan Chrome 155 da ishga tushirdik:

jsx
import { atom, useAtom, useAtomValue } from "jotai";
import { atomWithStorage } from "jotai/utils";

const langAtom = atomWithStorage("bahor:til", "uz");
const portionsAtom = atom(1);
const labelAtom = atom((get) =>
  get(langAtom) === "uz"
    ? `${get(portionsAtom)} porsiya`
    : `${get(portionsAtom)} portion(s)`,
);

function Label() {
  const label = useAtomValue(labelAtom);
  console.log("Label:", label);
  return <p>{label}</p>;
}

function Controls() {
  const [lang, setLang] = useAtom(langAtom);
  const [, setPortions] = useAtom(portionsAtom);
  return (
    <p>
      <button onClick={() => setPortions((n) => n + 1)}>
        +1
      </button>{" "}
      <button onClick={() => setLang(lang === "uz" ? "en" : "uz")}>
        Til
      </button>
    </p>
  );
}

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

«+1», keyin «Til» bosildi, so'ng sahifa qayta yuklandi. Konsol (qayta yuklashdan keyingi qism):

text
Label: 1 porsiya
Label: 1 portion(s)

localStorage da "en" bor edi (atomWithStorage qiymatni JSON qilib saqlaydi). Lekin birinchi chizish — baribir "porsiya" (boshlang'ich "uz"), keyin darhol "portion(s)". Bu Zustand'dagi SSR miltillashiga o'xshaydi, faqat bu yerda SSR ham yo'q. Sabab: sukut bo'yicha atomWithStorage xotirani birinchi chizishdan keyin o'qiydi (server va klient bir xil boshlashi uchun). SPA'da miltillamasligi uchun: atomWithStorage("bahor:til", "uz", undefined, { getOnInit: true }) — atom yaratilganda o'qiydi. Porsiya esa oddiy atom — sahifa yangilanganda 1 ga qaytdi.

Tekshirib ko'ring: const tableAtom = atom(4) va const labelAtom = atom((get) => \${get(tableAtom)}-stol`). store.set(tableAtom, 4)chaqirilsa,labelAtom` obunachisi xabar oladimi?

Javob

Yo'q. tableAtom ning qiymati 4 edi va yana 4 bo'ldi — Object.is(4, 4), o'zgarish yo'q. Hosila atom qayta hisoblanmaydi, obunachilar jim. Node misolidagi ikki marta 38 000 — xuddi shu holat.

3. Proxy modeli

3.1 Jonli model: Proxy bilan "o'zgartirib yoz"

Proxy va Reflect darsida Proxy — obyektga qilingan har o'qish va yozishni ushlaydigan "vositachi" ekanini ko'rgan edik. Proxy modelining g'oyasi: foydalanuvchi oddiy obyektni o'zgartiradi (order.portions += 1), set tuzog'i esa React'ga "o'zgardi" deydi. Kutubxonasiz o'quv modeli (bu Valtio emas):

jsx
import { useSyncExternalStore } from "react";

// O'quv modeli: proxy store (Valtio emas!)
function proxyStore(target) {
  let version = 0;
  const listeners = new Set();
  const state = new Proxy(target, {
    set(obj, key, value) {
      obj[key] = value;
      version += 1;
      console.log(`yozildi: ${String(key)} = ${value}`);
      listeners.forEach((listener) => listener());
      return true;
    },
  });
  const subscribe = (listener) => {
    listeners.add(listener);
    return () => listeners.delete(listener);
  };
  const useStore = () => {
    useSyncExternalStore(subscribe, () => version);
    return state;
  };
  return [state, useStore];
}

const [order, useOrder] = proxyStore({ dish: "Osh", portions: 1 });

export default function App() {
  const o = useOrder();
  return (
    <p>
      {o.dish}: {o.portions} porsiya{" "}
      <button onClick={() => (order.portions += 1)}>+1</button>
    </p>
  );
}

Konsolda:

text
yozildi: portions = 2
yozildi: portions = 3

Ekranda:

text
Osh: 3 porsiya +1

order.portions += 1 — oddiy JavaScript. Hech qanday set(...) yoki dispatch(...) yo'q. React baribir bildi: getSnapshot versiya raqamini qaytaradi, u har yozuvda o'sadi. Model sodda: faqat birinchi qavatni kuzatadi (order.items.push ni sezmaydi — ichki massiv proxy emas). Haqiqiy kutubxonalar har ichki obyektni ham proxy'ga o'raydi va kim nimani o'qiganini ham kuzatadi.

3.2 Valtio: proxy, snapshot, subscribe

Valtio (fincha "davlat") — proxy modelidagi kichik kutubxona, yana pmndrs jamoasidan. Versiyasi 2.3.2. React'siz qismi — valtio/vanilla:

js
import { proxy, snapshot, subscribe } from "valtio/vanilla";

const cart = proxy({
  items: [{ name: "Osh", price: 35000 }],
  waiter: "Sardor",
  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  },
});

subscribe(cart, () => console.log("o'zgardi, jami:", cart.total));

const before = snapshot(cart);
cart.items.push({ name: "Manti", price: 30000 });
cart.items[0].price = 38000;
await Promise.resolve();

const after = snapshot(cart);
console.log(before.total, after.total);
console.log("items yangi:", before.items !== after.items);
console.log("o'zgarmagan qism:", snapshot(cart) === after);

Node 24, valtio 2.3.2:

text
o'zgardi, jami: 68000
35000 68000
items yangi: true
o'zgarmagan qism: true
  • proxy(obyekt) — kuzatiladigan holat. Ichki massiv va obyektlar ham chuqur kuzatiladi: cart.items.push(…) va cart.items[0].price = … ikkalasi ham ushlandi.
  • subscribe — ikki o'zgarish uchun bir xabar. Valtio yozuvlarni bitta mikrovazifaga yig'adi (event loop dagi mikrovazifa navbati) — shuning uchun misolda await Promise.resolve() bor.
  • get total() — getter (Getter va setter) — hosila qiymat proxy ichida.
  • snapshot(cart) — shu ondagi o'zgarmas nusxa. O'zgargan qism — yangi havola (items yangi: true), o'zgarish bo'lmasa — o'sha nusxa qaytadi (o'zgarmagan qism: true). Bu strukturaviy bo'lishish: React Object.is bilan solishtira oladi.

Qoida: o'zgartirish — proxy'da, o'qish (renderda) — snapshot'da. Snapshot'ni o'zgartirmang: u faqat ko'rsatish uchun, o'zgarish hech qayerga yetmaydi.

3.3 useSnapshot — faqat o'qilgan maydonlar

React'da useSnapshot(proxy) snapshot qaytaradi va render paytida qaysi maydonlar o'qilganini eslab qoladi. Quyidagi ilovani valtio 2.3.2 bilan Chrome 155 da ishga tushirdik («Osh», keyin «Malika» bosildi):

jsx
import { proxy, useSnapshot } from "valtio";

const cart = proxy({ items: [], waiter: "Sardor" });

function Waiter() {
  const snap = useSnapshot(cart);
  console.log("Waiter chizildi:", snap.waiter);
  return <p>Ofitsiant: {snap.waiter}</p>;
}

function Count() {
  const snap = useSnapshot(cart);
  console.log("Count chizildi:", snap.items.length);
  return <p>Savatda: {snap.items.length}</p>;
}

function Buttons() {
  return (
    <p>
      <button onClick={() => cart.items.push("Osh")}>Osh</button>{" "}
      <button onClick={() => (cart.waiter = "Malika")}>Malika</button>
    </p>
  );
}

export default function App() {
  return (
    <>
      <Waiter />
      <Count />
      <Buttons />
    </>
  );
}
text
Waiter chizildi: Sardor
Count chizildi: 0
Count chizildi: 1
Waiter chizildi: Malika

Ikkala komponent bir xil useSnapshot(cart) chaqiradi — selektor yo'q. Lekin «Osh» bosilganda faqat Count (u items ni o'qigan), «Malika» da faqat Waiter chizildi. Valtio snapshot'ni ham proxy'ga o'rab, render paytidagi o'qishlarni yozib oladi (proxy-compare kutubxonasi). Bu avtomatik selektor: nimani o'qisangiz, shunga obunasiz.

Tuzog'i ham shundan: const { items } = useSnapshot(cart) dan keyin items.length ni faqat useEffect ichida o'qisangiz, render paytida o'qilmagan — Valtio uni kuzatmaydi. O'qishlar renderda bo'lsin.

3.4 MobX: observable, computed, autorun

MobX — proxy (reaktiv) modelining eng eski va katta kutubxonasi (2015). Versiyalari: mobx 7.0.6, React bog'lamasi mobx-react-lite 5.1.0. Uslubi — klasslar va "reaktiv" hisob:

js
import { autorun, makeAutoObservable } from "mobx";

class Cart {
  items = [];
  waiter = "Sardor";
  constructor() {
    makeAutoObservable(this);
  }
  add(name, price) {
    this.items.push({ name, price });
  }
  get total() {
    console.log("(total hisoblandi)");
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

const cart = new Cart();
autorun(() => console.log("ekran: jami", cart.total));

cart.add("Osh", 35000);
cart.waiter = "Malika";
cart.add("Manti", 30000);

Node 24, mobx 7.0.6:

text
(total hisoblandi)
ekran: jami 0
(total hisoblandi)
ekran: jami 35000
(total hisoblandi)
ekran: jami 65000
  • makeAutoObservable(this) — klassning maydonlarini observable (kuzatiladigan), getterlarini computed (eslab qolinadigan hosila), metodlarini action qiladi.
  • autorun(fn) — funksiyani darhol bajaradi va u o'qigan observable'lar o'zgarsa, qayta bajaradi. "ekran: jami" — React komponentining o'rinbosari.
  • Ofitsiant almashdi (cart.waiter = "Malika") — autorun jim. U waiter ni o'qimagan.
  • (total hisoblandi) har o'zgarishda bir marta: computed o'z bog'liqliklari (items) o'zgarganda qayta hisoblanadi, aks holda eslab qolingan qiymatni beradi.

React'da komponent observer(...) bilan o'raladi — u autorun ning komponent varianti. Kanon tajribasidagi MobX varianti (aynan):

tsx
// holat-mobx.tsx — MobX 7 + mobx-react-lite 5 (observable klass)
import { makeAutoObservable } from "mobx";
import { observer } from "mobx-react-lite";
import { Korinish, ishgaTushir } from "./_umumiy.tsx";
import type { Filtr, Vazifa } from "./_umumiy.tsx";

class VazifalarStore {
  vazifalar: Vazifa[] = [];
  filtr: Filtr = "hammasi";
  constructor() {
    makeAutoObservable(this);
  }
  qosh = (matn: string) => {
    this.vazifalar.push({ id: this.vazifalar.length + 1, matn, bajarildi: false });
  };
  almashtir = (id: number) => {
    const v = this.vazifalar.find((x) => x.id === id);
    if (v) v.bajarildi = !v.bajarildi;
  };
  filtrla = (f: Filtr) => {
    this.filtr = f;
  };
}
const store = new VazifalarStore();
const Ilova = observer(() => (
  <Korinish
    holat={{ vazifalar: store.vazifalar.slice(), filtr: store.filtr, qosh: store.qosh, almashtir: store.almashtir, filtrla: store.filtrla }}
  />
));
ishgaTushir(<Ilova />);

Tajriba eslatmasi: store.vazifalar.slice() — MobX massivi proxy, Korinish uni oddiy massiv kabi olsin deb nusxa. observer ichida bu nusxa olish ham "o'qish" — shuning uchun ro'yxat o'zgarsa, Ilova qayta chiziladi.

Tekshirib ko'ring: MobX misolida cart.add("Choy", 0) chaqirilsa, autorun qayta ishlaydimi? total qayta hisoblanadimi?

Javob

total — ha, autorun — yo'q. items massiviga element qo'shildi, ya'ni total ning bog'liqligi o'zgardi. U qayta hisoblanadi, natija esa o'sha 65 000. autorun faqat total ni o'qiydi. MobX computed natijasi o'zgarmaganini ko'radi va autorun ni qayta ishlatmaydi: computed qiymat teng bo'lsa, unga bog'liq reaksiyalar jim qoladi. Konsolda faqat (total hisoblandi) chiqadi, "ekran: jami" — yo'q.

4. Signals g'oyasi

Jotai'ning get(atom) i, Valtio'ning snapshot o'qishlari va MobX'ning observable'lari — bitta g'oyaning uch ko'rinishi: qiymat kim uni o'qiganini o'zi kuzatadi va faqat ularga xabar beradi. Bu g'oya signals deb ataladi. Preact, Solid, Vue (ref), Angular (signal()) va Svelte 5 ($state) uni o'z yadrosiga olgan. TC39 da (JavaScript standarti qo'mitasi) uni tilga qo'shish taklifi ham bor (proposal-signals) — hali standart emas.

Kutubxonasiz o'quv modeli — ishga tushiring:

js
let running = null; // hozir bajarilayotgan effekt

function signal(value) {
  const readers = new Set();
  return {
    get() {
      // kim o'qidi — yozib olamiz
      if (running) readers.add(running);
      return value;
    },
    set(next) {
      value = next;
      readers.forEach((effect) => effect());
    },
  };
}

function effect(fn) {
  running = fn;
  fn();
  running = null;
}

const price = signal(35000);
const portions = signal(1);
const waiter = signal("Sardor");

effect(() => console.log("jami:", price.get() * portions.get()));

portions.set(2);
waiter.set("Malika");
price.set(38000);

Konsolda:

text
jami: 35000
jami: 70000
jami: 76000

effect birinchi bajarilishda price va portions ni o'qidi — ular effektni "o'quvchi" sifatida yozib oldi. waiter ni o'qimagan — uning o'zgarishi jim o'tdi. Hech qanday selektor, bog'liqliklar massivi yo'q. Haqiqiy kutubxonalar bunga hosila signallar (computed), partiyalab yangilash va eski bog'liqliklarni tozalashni qo'shadi.

React o'zi signals modelini ishlatmaydi: unda "holat o'zgardi → komponent qayta chizildi → farq hisoblandi" (Render va commit). Shuning uchun React'da signals kutubxonalari useSyncExternalStore orqali ulanadi va komponent darajasida ishlaydi. Signals'ning asosiy foydasi — mayda yangilanishlar — React'da React Compiler va selektorlar bilan qisman qoplanadi.

5. Kanon tajribasi: yetti variant (nega kanonga kirmadi)

Bu darsda vazifalar o'zgarmaydi. Redux Toolkit I darsidagi tajriba — bir xil vazifa (qo'shish, belgilash, filtr, "qoldi") yetti vosita bilan — atom va proxy modellarini ham o'z ichiga oladi. Hajmni o'sha darsda ko'rdik. Bu safar — kod qatori (bo'sh va izoh qatorlarisiz, tajriba/hajm.txt):

Bir xil vazifa — holat kodi qatori
  • Zustand+0.4 KB14 qator
  • Jotai+2.9 KB19 qator
  • Valtio+1.7 KB22 qator
  • MobX+12.6 KB28 qator
  • Redux Toolkit+8.3 KB34 qator
  • useReducer + Contextasos39 qator
  • XState+12.9 KB42 qator

Manba: IlmHamroh kurs tajribasi (qator — bo'sh va izohsiz; KB — Context variantiga nisbatan gzip, 2026-10-13)

Jotai va Valtio variantlari (aynan):

tsx
// holat-jotai.tsx — Jotai 3 (atomlar)
import { atom, useAtom, useSetAtom } from "jotai";
import { Korinish, ishgaTushir } from "./_umumiy.tsx";
import type { Filtr, Vazifa } from "./_umumiy.tsx";

const vazifalarAtom = atom<Vazifa[]>([]);
const filtrAtom = atom<Filtr>("hammasi");
const qoshAtom = atom(null, (get, set, matn: string) =>
  set(vazifalarAtom, [...get(vazifalarAtom), { id: get(vazifalarAtom).length + 1, matn, bajarildi: false }]),
);
const almashtirAtom = atom(null, (get, set, id: number) =>
  set(vazifalarAtom, get(vazifalarAtom).map((v) => (v.id === id ? { ...v, bajarildi: !v.bajarildi } : v))),
);

function Ilova() {
  const [vazifalar] = useAtom(vazifalarAtom);
  const [filtr, filtrla] = useAtom(filtrAtom);
  const qosh = useSetAtom(qoshAtom);
  const almashtir = useSetAtom(almashtirAtom);
  return <Korinish holat={{ vazifalar, filtr, qosh, almashtir, filtrla }} />;
}
ishgaTushir(<Ilova />);
tsx
// holat-valtio.tsx — Valtio 2 (proxy: to'g'ridan o'zgartirish)
import { proxy, useSnapshot } from "valtio";
import { Korinish, ishgaTushir } from "./_umumiy.tsx";
import type { Filtr, Vazifa } from "./_umumiy.tsx";

const holat = proxy({ vazifalar: [] as Vazifa[], filtr: "hammasi" as Filtr });

function Ilova() {
  const s = useSnapshot(holat);
  return (
    <Korinish
      holat={{
        vazifalar: s.vazifalar,
        filtr: s.filtr,
        qosh: (matn) => void holat.vazifalar.push({ id: holat.vazifalar.length + 1, matn, bajarildi: false }),
        almashtir: (id) => {
          const v = holat.vazifalar.find((x) => x.id === id);
          if (v) v.bajarildi = !v.bajarildi;
        },
        filtrla: (f) => void (holat.filtr = f),
      }}
    />
  );
}
ishgaTushir(<Ilova />);

Jotai'da hamma narsa atom: hatto amallar (qoshAtom, almashtirAtom). Valtio'da amal — oddiy JS: holat.vazifalar.push(…), v.bajarildi = !v.bajarildi. Bu "o'zgartirib yoz" uslubi Immer bilan bir xil his beradi, lekin Immer'dagi "qoralama" chegarasi yo'q — istalgan joyda yozasiz.

Nega kanonga kirmadi: vazifalar ning client state'i — e'lon va ikki sozlama (Zustand II). Talab — marshrut action'idan React'siz yozish va selektor. Zustand buni eng kichik narx bilan bajardi (tajribada +0.4 KB, 14 qator; vazifalar build'ida — +1.3 KB) va persist, devtools tayyor. Atom yoki proxy modelining kuchli tomonlari (ko'p mayda bog'liq qiymatlar, chuqur ichma-ich obyektlar) bu ilovada yo'q.

Qachon qaysi biri — to'g'ri tanlov:

Vosita Qachon
Jotai ko'p mayda, bir-biridan hisoblanadigan qiymatlar (konstruktor, filtrlar paneli, jadval muharriri); holat komponent daraxtiga yaqin, har sahifada alohida (Provider bilan)
Valtio chuqur ichma-ich obyekt (sozlamalar daraxti, diagramma muharriri), "o'zgartirib yoz" jamoaga qulay; eng kichik (+1.7 KB)
MobX katta domen modeli klasslar bilan (CRM, moliya), murakkab hisoblangan qiymatlar; jamoa OOP'ga o'rgangan; React'dan tashqari ham ishlatiladi
Zustand bitta global store, hook'siz kirish, eng kam narx — vazifalar tanlovi

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Jotai: atom qiymati har chizishda boshlang'ichga qaytadi atom() komponent ichida yaratilgan — har renderda yangi atom atomni modul darajasida (yoki useMemo)
Jotai: atomWithStorage bilan birinchi kadr miltillaydi xotira birinchi chizishdan keyin o'qiladi { getOnInit: true } (SPA'da)
Valtio: o'zgartirdim, ekran yangilanmadi snapshot o'zgartirilgan, proxy emas yozish — cart.x = … (proxy), o'qish — snap.x
Valtio: maydon o'zgarsa ham komponent chizilmaydi maydon renderda emas, effektda o'qilgan o'qishni render ichiga
MobX: komponent yangilanmaydi observer(...) bilan o'ralmagan const C = observer(() => …)
MobX: [MobX] Since strict-mode is enabled, changing (observed) observable values without using an action is not allowed observable action'dan tashqarida o'zgartirildi metod (makeAutoObservable uni action qiladi) yoki runInAction

7. Mashqlar

Mashqlar kurs/mashqlar/19/26-atom-proxy/ da. 1- va 2-mashq Node'da (o'z papkangizda jotai@3.0.1, valtio@2.3.2, .mjs fayl); 3-mashq — saytda.

1-mashq (oson): hosila atom

Narx 30 000, porsiya 2. Jami — hosila atom. Bo'sh joyni to'ldiring va natijani ayting:

js
const totalAtom = atom(
  (get) => get(priceAtom) [___:*] get(portionsAtom),
);

Porsiya 3 bo'lgach store.get(totalAtom) →

Yechim
js
import { atom, createStore } from "jotai/vanilla";

const priceAtom = atom(30000);
const portionsAtom = atom(2);
const totalAtom = atom((get) => get(priceAtom) * get(portionsAtom));

const store = createStore();
console.log(store.get(totalAtom));
store.set(portionsAtom, 3);
console.log(store.get(totalAtom));

Node 24, jotai 3.0.1:

text
60000
90000

store.set(portionsAtom, 3) dan keyin totalAtom ni hech kim "yangila" demadi — hosila atom bog'liqligi o'zgarganini o'zi biladi.

2-mashq (o'rta): Valtio buyurtma

Valtio proxy bilan buyurtma: table (4), items, get total(). subscribe har xabarda <stol>-stol: <jami> so'm yozsin. Avval osh qo'shing (keyin mikrovazifa kutish), so'ng choy qo'shib, stolni 5 ga o'zgartiring. Nechta xabar chiqadi?

Yechim
js
import { proxy, subscribe } from "valtio/vanilla";

const order = proxy({
  table: 4,
  items: [],
  get total() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  },
});

subscribe(order, () =>
  console.log(`${order.table}-stol: ${order.total} so'm`),
);

order.items.push({ name: "Osh", price: 35000 });
await Promise.resolve();
order.items.push({ name: "Choy", price: 5000 });
order.table = 5;
await Promise.resolve();

Node 24, valtio 2.3.2:

text
4-stol: 35000 so'm
5-stol: 40000 so'm

Ikkita xabar: oshdan keyin bitta, choy va stol o'zgarishi — birgalikda bitta (bir mikrovazifa ichida). Ekranda ham shunday: React ikki o'zgarishni bitta chizishda ko'radi.

3-mashq (qiyin): o'quv modeliga hosila atom

«Jonli model: atom 15 qatorda» bo'limidagi atom ga hosila atom qo'shing: derived(fn, deps) — fn bog'liq atomlar qiymatidan hisoblaydi, deps — bog'liq atomlar massivi. U ham get/subscribe ga ega bo'lsin (yozish yo'q) va bog'liqlardan biri o'zgarsa obunachilarini xabardor qilsin. Ekranda: Jami: <narx × porsiya> so'm.

Ishora: derived ichida har bog'liq atomga subscribe qiling; qiymatni get() chaqirilganda hisoblang. getSnapshot bir xil kirishda bir xil qiymat qaytarsin — son bo'lgani uchun bu o'z-o'zidan bajariladi.

Yechim
jsx
import { useSyncExternalStore } from "react";

function atom(initial) {
  let value = initial;
  const listeners = new Set();
  return {
    get: () => value,
    set(next) {
      value = typeof next === "function" ? next(value) : next;
      listeners.forEach((listener) => listener());
    },
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

function derived(fn, deps) {
  const listeners = new Set();
  deps.forEach((dep) =>
    dep.subscribe(() => listeners.forEach((listener) => listener())),
  );
  return {
    get: () => fn(...deps.map((dep) => dep.get())),
    subscribe(listener) {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

const priceAtom = atom(35000);
const portionsAtom = atom(1);
const totalAtom = derived(
  (price, portions) => price * portions,
  [priceAtom, portionsAtom],
);

function Total() {
  const total = useSyncExternalStore(
    totalAtom.subscribe,
    totalAtom.get,
  );
  console.log("Total:", total);
  return <p>Jami: {total} so'm</p>;
}

export default function App() {
  return (
    <>
      <Total />
      <button onClick={() => portionsAtom.set((n) => n + 1)}>
        +1 porsiya
      </button>{" "}
      <button onClick={() => priceAtom.set(38000)}>
        Narx 38 000
      </button>
    </>
  );
}

Konsolda:

text
Total: 35000
Total: 70000
Total: 76000

Ekranda:

text
Jami: 76000 so'm
+1 porsiya Narx 38 000

Modelning soddaligi: deps ni qo'lda beramiz. Jotai'da get(atom) chaqiruvining o'zi bog'liqlikni e'lon qiladi (signals modelidagi running kabi) va obunalar ham dangasa: hech kim o'qimasa, hosila atom bog'liqlarga obuna bo'lmaydi.

4-mashq (tahlil): qaysi model?

Har vazifa uchun tanlang — Zustand, Jotai, Valtio yoki MobX — va asoslang:

  1. Onlayn "taom konstruktori": 12 ta tanlov, narx va kaloriya ulardan hisoblanadi, har taom sahifasida alohida konstruktor.
  2. Restoran zalining sxemasi: stollar, stullar, ularning holati — 4 qavat ichma-ich obyekt, sudrab joylash.
  3. Ilova bo'yi bitta "foydalanuvchi sozlamalari" va savat, marshrut action'idan yoziladi.
Yechim
  1. Jotai — mayda bog'liq qiymatlar (hosila atomlar), har sahifaga alohida Provider (har taomga o'z store'i).
  2. Valtio (yoki MobX) — chuqur ichma-ich obyektni stol.stullar[2].band = true deb o'zgartirish; spread piramidasi yo'q. Jamoa klasslarni afzal ko'rsa — MobX.
  3. Zustand — bitta global store, getState() bilan hook'siz yozish, persist. vazifalar qarori.

Har uchala javob "yagona to'g'ri" emas: hammasi har qanday vazifani bajara oladi. Tanlov — qaysi biri shu vazifada kamroq kod va kamroq tuzoq beradi.

8. Real ishda

  • Jotai — Next.js loyihalarida ko'p (Provider har so'rovga store beradi), tRPC va TanStack Query bilan integratsiya paketlari bor (jotai-tanstack-query).
  • Valtio — muharrir tipidagi ilovalar (diagramma, slayd, 3D sahna — react-three-fiber bilan birga), chuqur holat.
  • MobX — korporativ ilovalar, ayniqsa Angular yoki klassik OOP fonidagi jamoalar; MobX-State-Tree — uning qat'iy sxemali varianti.
  • Signals — React'dan tashqaridagi freymvorklar (Solid, Vue, Angular, Svelte 5) asosi. React'da Preact Signals'ning React adapteri bor, lekin rasmiy yo'nalish emas.
  • Intervyu savollari: "atom va store modeli farqi?", "Valtio'da nega snapshot o'zgartirilmaydi?", "MobX computed nima beradi?", "signals nima va React undan nimasi bilan farq qiladi?".

Xulosa

  • Atom modeli (Jotai): holat — mayda atomlar; hosila atom get(...) bilan bog'liqlarini o'zi kuzatadi; qiymat store'da, atom — tavsif.
  • atomWithStorage — saqlanadigan atom; SPA'da miltillamasligi uchun getOnInit: true.
  • Proxy modeli (Valtio, MobX): oddiy obyektni o'zgartirasiz, Proxy buni sezadi; o'qilgan maydonlarga avtomatik obuna.
  • Valtio: yozish — proxy'da, o'qish — snapshot'da; MobX: observable, computed, action, observer.
  • Signals — "qiymat o'quvchilarini o'zi biladi" g'oyasi; React yadrosi uni ishlatmaydi, kutubxonalar useSyncExternalStore orqali ulanadi.
  • Tajriba: Zustand +0.4 KB/14 qator, Valtio +1.7/22, Jotai +2.9/19, MobX +12.6/28 — vazifalar da Zustand; boshqalari — o'z vazifasida.

Keyingi dars: IndexedDB kutubxonalari React'da: idb-keyval va Dexie — localStorage cheklovlari, katta ma'lumot va Blob'larni saqlash, useLiveQuery va sxema migratsiyasi; o'lchov bilan.

Manbalar

  • Jotai: Core (atom, useAtom, createStore), Utils (atomWithStorage) — jotai.org/docs (v3, 2026-10 holatiga)
  • Valtio: proxy, useSnapshot, subscribe, snapshot — valtio.dev/docs (v2)
  • MobX: makeAutoObservable, computed, autorun, observer — mobx.js.org (v7)
  • TC39 Signals proposal — github.com/tc39/proposal-signals
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Jotai, Valtio va MobX: atom va proxy state React'da, signals g'oyasi — IlmHamroh