IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar27/36-dars20 daqiqa
Mundarija (24)

IndexedDB React'da: idb-keyval va Dexie bilan brauzerda ma'lumot saqlash

Qisqacha: localStorage — sinxron, faqat matn, ~5 MB: kichik sozlamalar va qoralamalar uchun. Rasm (Blob), minglab yozuv yoki qidiriladigan ma'lumot — IndexedDB ga. Uning xom API'si noqulay, shuning uchun ikki kutubxona: idb-keyval — "kalit → qiymat" (get/set), Dexie — jadvallar, indekslar, so'rovlar, versiya migratsiyasi va React uchun useLiveQuery. Kanon o'lchovi (10 000 vazifa, Chrome 155): localStorage o'qish 1.7 ms, idb-keyval 6.5 ms, Dexie toArray 135.8 ms. Shuning uchun vazifalar ro'yxati localStorage da qoladi, IndexedDB — faqat profil rasmi uchun.

Bu darsda

  • localStorage ning to'rt cheklovini ko'rasiz: matn, JSON yo'qotishlari, kvota va sinxronlik.
  • idb-keyval bilan Blob, Date va Map ni saqlaysiz va React'ga useSyncExternalStore bilan ulaysiz.
  • Dexie'da jadval va indeks e'lon qilasiz, where bilan so'raysiz, useLiveQuery bilan ekranni avtomatik yangilaysiz.
  • Sxema versiyasini oshirib, eski yozuvlarni migratsiya qilasiz.
  • Boshqa tabdagi o'zgarishni (storage hodisasi) ushlaysiz.
  • Qaysi ma'lumot qayerda saqlanishini o'lchov bilan tanlaysiz.

Oldin bilishingiz kerak: localStorage va sessionStorage, IndexedDB va brauzer xotira kvotasi, Zustand II, useSyncExternalStore.

1. Nega bu kerak?

Jasur aka «Bahor» ofitsiantlari uchun planshet ilovasini xohlaydi. Zalda Wi-Fi ba'zan uziladi. Ofitsiant buyurtmani baribir qabul qilishi kerak — buyurtmalar brauzerda saqlanib, internet qaytganda serverga ketsin. Har buyurtmaga stol rasmi ham biriktiriladi.

Sardor hammasini localStorage ga yozmoqchi edi. Birinchi kunning o'zida uch muammo chiqdi: rasm localStorage ga sig'madi; buyurtma vaqti (Date) qayta o'qilganda oddiy satrga aylandi; 2 000 ta buyurtmadan keyin har saqlashda ekran bir lahza "qotadigan" bo'ldi.

localStorage va IndexedDB darslarida ikkala saqlash joyining xom API'sini o'rgandik. Bugun savol boshqa: React ilovada ularni qanday ishlatish, qaysi kutubxona bilan va qachon. Hayotiy o'xshatish: cho'ntak daftarcha va arxiv shkafi. Daftarcha (localStorage) doim qo'l ostida — ochasiz-u, o'qiysiz; lekin unga faqat yozuv sig'adi va varaqlari kam. Arxiv (IndexedDB) — hamma narsa sig'adi, papkalar va ko'rsatkichlar bor; lekin kerakli hujjatni so'rab, bir oz kutasiz.

2. localStorage ning cheklovlari

2.1 Faqat matn: JSON nimani yo'qotadi

localStorage faqat satr saqlaydi. Obyektni JSON.stringify bilan matnga aylantiramiz — va ba'zi narsalar yo'lda yo'qoladi. Ishga tushiring:

js
const order = {
  createdAt: new Date(2026, 9, 14, 12, 30),
  items: new Map([["osh", 2]]),
  note: undefined,
  table: 4,
};

const restored = JSON.parse(JSON.stringify(order));
console.log(typeof restored.createdAt); // string
console.log(restored.items); // {}
console.log("note" in restored); // false
console.log(restored.table); // 4

Sana — satrga, Map — bo'sh obyektga aylandi, undefined maydon butunlay yo'qoldi. Faqat oddiy son va satrlar sog' qaytdi. Structured clone (Web Worker darsida ko'rgan algoritm) esa bularning hammasini saqlaydi — IndexedDB aynan shu algoritm bilan yozadi:

js
const order = {
  createdAt: new Date(2026, 9, 14, 12, 30),
  items: new Map([["osh", 2]]),
};

const copy = structuredClone(order);
console.log(copy.createdAt instanceof Date); // true
console.log(copy.items.get("osh")); // 2

2.2 Rasm: base64 va +33%

Rasm — ikkilik ma'lumot (Blob). localStorage ga uni faqat base64 satrga aylantirib qo'yish mumkin. Base64 har 3 baytni 4 ta belgi bilan yozadi:

js
const bytes = new Uint8Array(300 * 1024); // 300 KB "rasm"
let binary = "";
for (const b of bytes) binary += String.fromCharCode(b);

const base64 = btoa(binary);
console.log(bytes.length); // 307200
console.log(base64.length); // 409600
console.log((base64.length / bytes.length).toFixed(2)); // 1.33

300 KB rasm — 400 KB matn. Chrome'da bitta sayt uchun localStorage ~5 MB (aniqrog'i, kalit va qiymatlar uzunligi bo'yicha), ya'ni 12 ta shunday rasm — va joy tugadi. Tugaganda nima bo'lishini Chrome 155 da sinadik (6 MB satr yozildi):

text
QuotaExceededError
Failed to execute 'setItem' on 'Storage': Setting the value of 'katta' exceeded the quota.

Tarjimasi: "Storage da setItem bajarilmadi: katta qiymati kvotadan oshdi." Bu istisno: try/catch siz setItem ilovani yiqitadi. localStorage ga har yozuv try ichida bo'lishi kerak (localStorage darsidagi qoida).

2.3 Sinxronlik — kuchi ham, zaifligi ham

localStorage.getItem sinxron: natija shu qatorning o'zida, await siz. Bu — ham yutuq, ham xavf:

  • Yutuq: ilova birinchi chizishdan oldin saqlangan holatni o'qiy oladi. Zustand II dagi persist aynan shuning uchun birinchi kadrdayoq to'g'ri sozlama beradi.
  • Xavf: katta ma'lumotda o'qish ham, yozish ham asosiy oqimni band qiladi — shu vaqt ichida bosish, yozish, animatsiya to'xtaydi.

Qanchalik katta? Raqamlar — «Kanon o'lchovi» bo'limida.

2.4 sessionStorage va boshqa tab

sessionStorage — xuddi shunday API, lekin tab yopilguncha yashaydi va har tabning o'zi uchun alohida. To'lov oynasidagi vaqtinchalik qadam, "bu tabda ko'rsatilgan e'lonni qayta ko'rsatma" kabi narsalar uchun. Kanonda u vazifalar:tarix (Orqaga tugmasi uchun tarix chuqurligi, React Router III) da ishlatiladi.

localStorage esa saytning hamma tablari uchun umumiy. Bir tab yozsa, boshqalariga storage hodisasi keladi (yozgan tabning o'ziga — kelmaydi). Zustand II da kanon shu hodisa bilan sozlamalarni qayta o'qiydi. Kutubxonasiz React hook'i qanday bo'lishini 2-mashqda yozasiz.

Tekshirib ko'ring: Ofitsiant planshetida «oxirgi tanlangan zal» (bir so'z), «yozilayotgan buyurtma qoralamasi» (shu tab uchun) va «stol rasmlari» saqlanadi. Har biri qayerga?

Javob

Zal — localStorage (kichik matn, keyingi safar ham kerak, birinchi kadrda o'qiladi). Qoralama — sessionStorage (faqat shu tab va shu sessiya; ikkinchi tabda boshqa buyurtma yozilishi mumkin). Rasmlar — IndexedDB (Blob, katta, base64'siz).

3. idb-keyval: kalit va qiymat

3.1 Eng kichik IndexedDB o'rami

IndexedDB darsida xom API'ni ko'rdik: open, upgradeneeded, tranzaksiya, onsuccess. Sizga faqat "kalit bo'yicha qiymat saqlash" kerak bo'lsa — bularning hammasi ortiqcha. idb-keyval — Jake Archibald yozgan kichik kutubxona: localStorage ga o'xshash API, lekin IndexedDB ustida va async. Kanondagi versiya — 6.3.0.

Saytdagi natija oynasida IndexedDB ishlamaydi (sandbox'li iframe — shaffof origin). Quyidagi kodni idb-keyval 6.3.0 bilan Chrome 155 da ishga tushirdik:

jsx
import { get, keys, set } from "idb-keyval";

async function main() {
  const photo = new Blob([new Uint8Array(300 * 1024)], {
    type: "image/webp",
  });
  await set("bahor:rasm", photo);
  await set("bahor:buyurtma", {
    createdAt: new Date(2026, 9, 14, 12, 30),
    items: new Map([["osh", 2]]),
  });

  const saved = await get("bahor:rasm");
  console.log(saved instanceof Blob, saved.type, saved.size);
  const order = await get("bahor:buyurtma");
  const isDate = order.createdAt instanceof Date;
  console.log(isDate, order.items.get("osh"));
  console.log(JSON.stringify(await keys()));

  try {
    await set("bahor:funksiya", { pay: () => "to'landi" });
  } catch (error) {
    console.log(error.name);
  }
}
main();
text
true image/webp 307200
true 2
["bahor:buyurtma","bahor:rasm"]
DataCloneError
  • Blob o'zi saqlandi: turi va hajmi (307 200 bayt) bilan. Base64 yo'q, +33% yo'q.
  • Date — Date bo'lib, Map — Map bo'lib qaytdi (structured clone).
  • keys() — barcha kalitlar.
  • Funksiyali obyekt — DataCloneError: structured clone funksiyani nusxalay olmaydi. JSON jim tashlab yuborardi, IndexedDB esa ochiq xato beradi.

API: get, set, del, update(kalit, (eski) => yangi) (o'qish va yozish bitta tranzaksiyada), keys, values, entries, clear, createStore (alohida baza / object store nomi bilan). Hammasi promise qaytaradi.

Zustand persist ni IndexedDB'ga ulash. Zustand II da createJSONStorage(() => sessionStorage) ni ko'rgan edik. U getItem/setItem/removeItem li har qanday saqlash joyini (storage) oladi — async bo'lsa ham. idb-keyval bilan (Zustand hujjatidagi "custom storage" namunasi asosida):

js
import { del, get, set } from "idb-keyval";
import { createJSONStorage } from "zustand/middleware";

export const idbStorage = createJSONStorage(() => ({
  getItem: async (name) => (await get(name)) ?? null,
  setItem: (name, value) => set(name, value),
  removeItem: (name) => del(name),
}));
// persist(..., { name: "bahor:savat", storage: idbStorage })

Narxi — async tiklanish: store birinchi kadrda boshlang'ich qiymat bilan chiziladi, saqlangan holat esa bir lahzadan keyin keladi. localStorage dagi sinxron tiklanish (birinchi kadrdayoq to'g'ri qiymat) yo'qoladi. Shuning uchun kanonda sozlamalar localStorage da qoldi.

3.2 React'ga ulash: kanondagi profil rasmi

idb-keyval'ning React hook'i yo'q va kerak ham emas: IndexedDB — tashqi manba, ulash usuli bizga useSyncExternalStore dan tanish. Kanonda profil rasmi aynan shunday ishlaydi (profil-rasmi.ts, holat 36 — fayl yuklash UI darsida to'liq ko'ramiz, Fayl yuklash UI). Bugungi mavzuga tegishli qismi (kanon, izohlar qisqartirilgan):

ts
import { useSyncExternalStore } from "react";
import { del, get, set } from "idb-keyval";

const KALIT = "vazifalar:profil-rasmi";

let joriyUrl: string | null = null;
let yuklandi = false;
const tinglovchilar = new Set<() => void>();

function almashtir(blob: Blob | undefined): void {
  if (joriyUrl !== null) {
    URL.revokeObjectURL(joriyUrl); // eski Blob xotiradan bo'shasin
  }
  joriyUrl = blob === undefined ? null : URL.createObjectURL(blob);
  tinglovchilar.forEach((t) => t());
}

function obuna(tinglovchi: () => void): () => void {
  tinglovchilar.add(tinglovchi);
  if (!yuklandi) {
    yuklandi = true;
    // Birinchi obunachi — IndexedDB'dan (async; yo'q yoki xato —
    // rasmsiz). Promise.resolve().then: IndexedDB umuman yo'q
    // bo'lsa get() SINXRON tashlaydi — u ham rad etilgan Promise
    // bo'lsin, menyu yiqilmasin (Vitest/jsdom topdi)
    Promise.resolve()
      .then(() => get<Blob>(KALIT))
      .then(almashtir, (xato: unknown) => {
        console.warn(`Profil rasmi o'qilmadi: ${String(xato)}`);
      });
  }
  return () => tinglovchilar.delete(tinglovchi);
}

/** blob: URL yoki null (rasm yo'q) */
export function useProfilRasmi(): string | null {
  return useSyncExternalStore(
    obuna,
    () => joriyUrl,
    () => null,
  );
}

export async function rasmniSaqla(blob: Blob): Promise<void> {
  await set(KALIT, blob);
  almashtir(blob);
}

Qatorma-qator nimani o'rgatadi:

  1. Dangasa o'qish. IndexedDB birinchi obunachi paydo bo'lganda o'qiladi (yuklandi), modul yuklanishida emas. Rasm kerak bo'lmagan sahifa bazani ochmaydi.
  2. Async — demak birinchi kadrda rasm yo'q. getSnapshot boshida null beradi, rasm o'qilgach — blob: URL. Profil rasmi uchun bu maqbul (avatar bir lahzadan keyin paydo bo'ladi). Ro'yxat uchun esa — yo'q: bo'sh ro'yxat → keyin to'la ro'yxat — miltillash va CLS.
  3. URL.createObjectURL — Blob'ga blob: manzil beradi, <img src> ga qo'yiladi. Eski URL revokeObjectURL bilan bo'shatiladi, aks holda Blob xotirada qoladi. CSP'ga img-src 'self' blob: qo'shilgani shundan — sababini Fayl yuklash UI darsida batafsil ko'ramiz.
  4. Haqiqiy topilma. Vitest muhitida (jsdom) indexedDB umuman yo'q. Shunda get() promise qaytarish o'rniga sinxron tashlar edi va butun foydalanuvchi menyusi yiqilardi. Promise.resolve().then(() => get(…)) sinxron xatoni ham rad etilgan promise'ga aylantiradi. Xuddi shu holat haqiqiy hayotda ham bor: Firefox'ning ba'zi maxfiy rejimlari va o'chirilgan sayt ma'lumotlari.

Tekshirib ko'ring: Nega getServerSnapshot (uchinchi argument) () => null?

Javob

Serverda (SSR) IndexedDB ham, Blob URL ham yo'q. Server rasmsiz chizadi. Jonlantirishda React null bilan solishtiradi — mos keladi. Keyin klient rasm o'qilgach yangilaydi. vazifalar da SSR yo'q, lekin hook to'g'ri yozilgan: kelajakda Next.js'ga ko'chsa, hydration xatosi bermaydi (Zustand II dagi getServerSnapshot mantig'i).

4. Dexie: jadvallar, so'rovlar, migratsiya

4.1 Jadval va indeks

Ofitsiant planshetiga "arzon taomlar", "7-stolning buyurtmalari", "to'lanmaganlar" kabi so'rovlar kerak. idb-keyval'da buning uchun hammasini o'qib, JavaScript'da filter qilasiz. Dexie — IndexedDB ustidagi to'liq qatlam: jadvallar (object store), indekslar va so'rovlar. Tajribadagi versiya — dexie 4.4.6, React hook'lari — dexie-react-hooks 4.4.0.

Sxema bitta satr bilan e'lon qilinadi: "++id, price" — birinchisi asosiy kalit (++ — avtomatik o'suvchi), qolganlari — indekslangan maydonlar. Indekslanmagan maydonlar ham saqlanadi, lekin ular bo'yicha where qilib bo'lmaydi.

4.2 Versiya va migratsiya

Ilova o'sadi, sxema o'zgaradi. Zustand II da persist ning version + migrate ini ko'rdik; IndexedDB'da g'oya o'sha, faqat brauzer darajasida: baza versiyasi oshsa, upgradeneeded (IndexedDB) ishga tushadi. Dexie uni version(n).stores(...).upgrade(fn) ga o'raydi. Quyidagi kod ikki "relizni" ketma-ket taqlid qiladi — dexie 4.4.6, Chrome 155:

jsx
import Dexie from "dexie";

async function main() {
  // 1-versiya: taomlar jadvali, indeks — narx
  const v1 = new Dexie("bahor");
  v1.version(1).stores({ dishes: "++id, price" });
  await v1.dishes.bulkAdd([
    { name: "Osh", price: 35000 },
    { name: "Manti", price: 30000 },
    { name: "Ko'k choy", price: 5000 },
  ]);
  const cheap = await v1.dishes.where("price").below(31000).toArray();
  console.log("arzon:", cheap.map((d) => d.name).join(", "));
  v1.close();

  // 2-versiya (keyingi reliz): yangi indeks, eskilarini ko'chirish
  const v2 = new Dexie("bahor");
  v2.version(1).stores({ dishes: "++id, price" });
  v2.version(2)
    .stores({ dishes: "++id, price, category" })
    .upgrade(async (tx) => {
      const count = await tx
        .table("dishes")
        .toCollection()
        .modify((dish) => {
          dish.category = dish.price > 10000 ? "taom" : "ichimlik";
        });
      console.log("migratsiya: ko'chirildi", count);
    });
  const drinks = await v2.dishes
    .where("category")
    .equals("ichimlik")
    .toArray();
  console.log("ichimliklar:", drinks.map((d) => d.name).join(", "));
  console.log("versiya:", v2.verno);
}
main();
text
arzon: Ko'k choy, Manti
migratsiya: ko'chirildi 3
ichimliklar: Ko'k choy
versiya: 2
  • where("price").below(31000) — indeks bo'yicha so'rov: baza hamma yozuvni o'qimaydi, indeksdan kerakli oraliqni oladi. Natija indeks tartibida (narx o'sishi bo'yicha).
  • 2-versiyada category indeksi qo'shildi va upgrade eski uchta yozuvga kategoriya yozdi. Bu funksiya faqat bir marta — eski versiyali baza birinchi ochilganda — ishlaydi.
  • version(1) 2-versiyali kodda ham qoladi: Dexie versiyalar tarixini ko'radi (eski versiyalarni o'chirish mumkin, agar ular faqat stores bo'lsa, upgrade siz).

Qoida o'sha: sxema o'zgardi — versiya oshdi — eski ma'lumot ko'chirildi. Ko'chirish unutilsa, eski foydalanuvchida yangi indeks bo'yicha so'rov bo'sh qaytadi (eski yozuvlarda category yo'q).

4.3 useLiveQuery — baza o'zgarsa, ekran yangilanadi

Dexie'ning React uchun eng kuchli imkoniyati — jonli so'rov: useLiveQuery(() => db.items.toArray()). Komponent so'rov natijasini oladi va so'rovga tegishli jadval har qanday joydan o'zgarsa (boshqa komponent, boshqa tab, servis ishchisi), qayta chiziladi. Chrome 155, dexie-react-hooks 4.4.0 — «Osh» ikki marta bosildi, keyin sahifa yangilandi:

jsx
import Dexie from "dexie";
import { useLiveQuery } from "dexie-react-hooks";

const db = new Dexie("bahor-savat");
db.version(1).stores({ items: "++id, name" });

function Cart() {
  const items = useLiveQuery(() => db.items.toArray());
  if (items === undefined) {
    console.log("Cart: yuklanmoqda");
    return <p>Yuklanmoqda…</p>;
  }
  console.log("Cart:", items.length);
  const names = items.map((item) => item.name).join(", ");
  return <p>Savat: {names || "bo'sh"}</p>;
}

function AddButton() {
  // komponent store'ni bilmaydi: bazaga yozadi, xolos
  return (
    <button onClick={() => db.items.add({ name: "Osh" })}>Osh</button>
  );
}

export default function App() {
  return (
    <>
      <Cart />
      <AddButton />
    </>
  );
}
text
Cart: yuklanmoqda
Cart: 0
Cart: 1
Cart: 2
--- (sahifa qayta yuklandi)
Cart: yuklanmoqda
Cart: 2
  • Birinchi chizishda — undefined ("yuklanmoqda"): so'rov async. Har sahifa ochilishida shunday bo'ladi — oxirgi ikki qatorga qarang.
  • AddButton faqat bazaga yozdi. Hech qanday store, setState, invalidate yo'q — Cart o'zi yangilandi: Dexie qaysi jadval o'zgarganini kuzatadi.
  • Ma'lumot sahifa yangilangandan keyin ham joyida — ikki osh.

Bu — Jotai, Valtio, MobX darsidagi "qiymat o'quvchilarini o'zi biladi" g'oyasining baza darajasidagi ko'rinishi. Boshqa tabdagi o'zgarish ham yetib keladi: Dexie tablar orasida xabar almashadi (Oyna va tablar orasida aloqa darsidagi kanallar g'oyasi).

Tekshirib ko'ring: useLiveQuery(() => db.items.where("name").equals("Osh").count()) — bazaga «Manti» qo'shilsa, komponent qayta chiziladimi?

Javob

Dexie 4 so'rov qaysi indeks oralig'ini o'qiganini kuzatadi: name = "Osh". «Manti» bu oraliqqa tushmaydi — so'rov qayta bajarilmaydi, komponent chizilmaydi. «Osh» qo'shilsa — yangilanadi. Indekssiz, butun jadvalni o'qiydigan so'rov (toArray()) esa har o'zgarishda qayta bajariladi.

5. Kanon o'lchovi: qaysi ma'lumot qayerda

Bu darsda vazifalar ga yangi kod yo'q. Lekin "ro'yxatni IndexedDB'ga ko'chiramizmi?" savolini o'lchov bilan hal qildik (natijalar/tajriba/27-indexeddb.txt, Chrome 155, har usul 7 marta, mediana). 10 000 vazifa (JSON 830 KB):

10 000 vazifa: saqlash va o'qish vaqti (mediana)
  • localStorage o'qishsinxron ≤ 2.4 ms1,7 ms
  • idb-keyval getasync6,5 ms
  • idb-keyval setasync7,4 ms
  • localStorage yozishsinxron ≤ 11 ms9,4 ms
  • Dexie toArrayhar vazifa — yozuv135,8 ms
  • Dexie bulkPutasosiy oqim ≤ 103 ms band309,6 ms

Manba: IlmHamroh kurs tajribasi (Chrome 155.0.8059.40, idb-keyval 6.3.0, dexie 4.4.6)

Xom fayldagi "bitta vazifani yangilash" qatorlari:

text
Dexie bitta vazifani yangilash (update)                        1.9 ms   sinxron ≤ 1.2 ms
idb-keyval bitta vazifani yangilash (butun massiv qayta)      21.5 ms   sinxron ≤ 0.2 ms

Nimani ko'ramiz:

  • O'qish: localStorage eng tez (1.7 ms) va sinxron. 830 KB JSON ni JSON.parse qilish ham shu ichida.
  • idb-keyval butun massivni bitta kalitda saqlaydi — o'qish 6.5 ms, lekin async: birinchi chizishga ulgurmaydi.
  • Dexie har vazifani alohida yozuv qiladi: hammasini o'qish (135.8 ms) va yozish (309.6 ms) ancha sekin, bulkPut asosiy oqimni 103 ms gacha band qildi. Lekin bitta vazifani yangilash — 1.9 ms, idb-keyval'da esa butun massivni qayta yozish kerak — 21.5 ms.

Kanon qarori: vazifalar ro'yxati localStorage da qoladi — birinchi kadr sinxron o'qishga tayanadi va 10 000 vazifada ham 1.7 ms. IndexedDB (idb-keyval) — faqat profil rasmi uchun (Blob; base64 +33% va kvota). Dexie kanonga kirmadi: so'rov va indekslar kerak emas (qidiruv — xotirada), butun ro'yxatni yuklash esa sekinroq.

flowchart TD
  A["Nima saqlanadi?"] --> B{"Rasm, fayl, Blob?"}
  B -- ha --> I["IndexedDB: idb-keyval"]
  B -- yo'q --> C{"Kichik va birinchi kadrda kerak?"}
  C -- ha --> L["localStorage (shu tab uchun — sessionStorage)"]
  C -- yo'q --> D{"Ko'p yozuv, so'rov, bittalab yangilash?"}
  D -- ha --> X["IndexedDB: Dexie"]
  D -- yo'q --> I

Qachon Dexie — to'g'ri tanlov: oflayn ishlaydigan ilovalar (ofitsiant plansheti, kuryer ilovasi) — minglab yozuv, indeks bo'yicha so'rov, bittalab yangilash, tablararo jonli yangilanish. Bunday ilovada sinxron birinchi kadr o'rniga "yuklanmoqda" holati qabul qilinadi.

6. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
QuotaExceededError va ilova yiqildi localStorage ~5 MB to'ldi (ko'pincha base64 rasm) Blob — IndexedDB'ga; setItem — try ichida
saqlangan sana — satr, Map — {} JSON structured clone emas IndexedDB yoki o'qishda qayta tiklash
DataCloneError IndexedDB'ga funksiya yoki DOM element faqat ma'lumot saqlang
ReferenceError: indexedDB is not defined (test) jsdom'da IndexedDB yo'q, get() sinxron tashladi Promise.resolve().then(() => get(…)) yoki fake-indexeddb
yangi indeks bo'yicha so'rov bo'sh versiya oshdi, eski yozuvlar ko'chirilmadi .upgrade(tx => …modify(…))
useLiveQuery natijasi undefined birinchi chizish — so'rov hali tugamagan "yuklanmoqda" holatini chizing
boshqa tabdagi o'zgarish ko'rinmaydi storage hodisasi tinglanmagan addEventListener("storage", …) yoki Dexie liveQuery
blob: rasm ko'rinmaydi CSP img-src da blob: yo'q img-src 'self' blob:

7. Mashqlar

Mashqlar kurs/mashqlar/19/27-indexeddb/ da. 2- va 3-mashq saytda ishlaydi (natija oynasida localStorage o'rinbosari bor).

1-mashq (oson): qayerga?

Har ma'lumotga saqlash joyi tanlang — localStorage, sessionStorage yoki IndexedDB:

  • tema ("qora"/"oq") →
  • to'lov oynasidagi joriy qadam (faqat shu tab) →
  • 300 KB lik taom rasmi →
  • 5 000 ta oflayn buyurtma, stol bo'yicha qidiriladi →
Yechim

Tema — kichik matn, birinchi kadrda kerak (aks holda oq ekran → qora ekran). To'lov qadami — faqat shu tab, tab yopilsa kerak emas. Rasm — Blob, base64 +33% va kvota. Buyurtmalar — ko'p yozuv va indeks bo'yicha so'rov: Dexie. Rasm uchun esa idb-keyval yetarli — so'rov kerak emas.

2-mashq (o'rta): useLocalStorage hook

localStorage dagi qiymatni o'qiydigan va yozadigan hook yozing: const [value, setValue] = useLocalStorage(kalit, boshlang'ich).

  1. Qiymat JSON bo'lib saqlanadi; buzuq JSON yoki yo'q kalit — boshlang'ich qiymat.
  2. Bir xil kalitli ikki komponent bir-birini ko'rsin: biri yozsa, ikkinchisi yangilansin.
  3. Boshqa tabdan kelgan storage hodisasi ham yangilasin.

Ishora: useSyncExternalStore — getSnapshot satrni qaytarsin (satrlar qiymat bo'yicha solishtiriladi), JSON.parse — undan keyin. O'z tabidagi yozuvni xabar qilish uchun o'zingizning Set ingiz.

Yechim
jsx
import { useSyncExternalStore } from "react";

const listeners = new Set();

function subscribe(listener) {
  listeners.add(listener);
  // boshqa tablar
  addEventListener("storage", listener);
  return () => {
    listeners.delete(listener);
    removeEventListener("storage", listener);
  };
}

function useLocalStorage(key, initial) {
  const raw = useSyncExternalStore(subscribe, () =>
    localStorage.getItem(key),
  );
  let value = initial;
  try {
    if (raw !== null) value = JSON.parse(raw);
  } catch {
    value = initial;
  }
  const setValue = (next) => {
    try {
      localStorage.setItem(key, JSON.stringify(next));
    } catch (error) {
      console.log("saqlanmadi:", error.name);
    }
    listeners.forEach((listener) => listener()); // shu tab
  };
  return [value, setValue];
}

function Toggle() {
  const [compact, setCompact] = useLocalStorage(
    "bahor:ixcham",
    false,
  );
  return (
    <button onClick={() => setCompact(!compact)}>Ixcham</button>
  );
}

function Status() {
  const [compact] = useLocalStorage("bahor:ixcham", false);
  return <p>Menyu: {compact ? "ixcham" : "oddiy"}</p>;
}

localStorage.setItem("bahor:buzuq", "{osh");

function Broken() {
  const [value] = useLocalStorage("bahor:buzuq", "standart");
  return <p>Buzuq kalit: {value}</p>;
}

export default function App() {
  return (
    <>
      <Toggle />
      <Status />
      <Broken />
    </>
  );
}

Ekranda:

text
Ixcham
Menyu: ixcham
Buzuq kalit: standart

Toggle yozdi, Status — boshqa komponent — yangilandi: ikkalasi bitta tashqi manbaga obuna. getSnapshot satr qaytaradi: agar JSON.parse natijasini (obyektni) qaytarganida, har chaqiruvda yangi obyekt — Zustand I dagi cheksiz sikl. Buzuq JSON ilovani yiqitmadi.

3-mashq (qiyin): o'z migratsiyangiz

Savat localStorage da { version, items } shaklida saqlanadi. Tarixi:

  • 1-versiya: items — nomlar: ["Osh"].
  • 2-versiya: items — { name, qty }.
  • 3-versiya (hozirgi): items — { name, qty, price }; eski yozuvlarda narx yo'q — PRICES jadvalidan oling.

loadCart() yozing: istalgan eski versiyani 3-versiyaga zinapoya bilan ko'chirsin va natijani qayta saqlasin. Boshlang'ich kodda brauzerda 1-versiyali yozuv bor.

jsx
localStorage.setItem(
  "bahor:savat",
  JSON.stringify({ version: 1, items: ["Osh", "Manti"] }),
);

const PRICES = { Osh: 35000, Manti: 30000 };
const VERSION = 3;

function loadCart() {
  const saved = JSON.parse(localStorage.getItem("bahor:savat"));
  // shu yerga: 1 → 2 → 3
  return saved.items;
}

export default function App() {
  const items = loadCart();
  return <p>Savatda: {items.length} xil</p>;
}

Ekranda:

text
Savatda: 2 xil
Yechim
jsx
localStorage.setItem(
  "bahor:savat",
  JSON.stringify({ version: 1, items: ["Osh", "Manti"] }),
);

const PRICES = { Osh: 35000, Manti: 30000 };
const VERSION = 3;

function loadCart() {
  let saved = JSON.parse(localStorage.getItem("bahor:savat"));
  if (saved.version < 2) {
    saved = {
      version: 2,
      items: saved.items.map((name) => ({ name, qty: 1 })),
    };
  }
  if (saved.version < 3) {
    saved = {
      version: 3,
      items: saved.items.map((item) => ({
        ...item,
        price: PRICES[item.name] ?? 0,
      })),
    };
  }
  if (saved.version !== VERSION) throw new Error("Noma'lum versiya");
  localStorage.setItem("bahor:savat", JSON.stringify(saved));
  return saved.items;
}

// bir marta, modul yuklanganda (renderda emas)
const items = loadCart();
console.log(localStorage.getItem("bahor:savat"));

export default function App() {
  const total = items.reduce((s, i) => s + i.qty * i.price, 0);
  return <p>Savatda: {items.length} xil, {total} so'm</p>;
}

Konsolda:

text
{"version":3,"items":[{"name":"Osh","qty":1,"price":35000},{"name":"Manti","qty":1,"price":30000}]}

Ekranda:

text
Savatda: 2 xil, 65000 so'm

Zinapoya if (version < N) — else if emas: 1-versiyali yozuv ikkala qadamdan o'tadi, 2-versiyali — faqat ikkinchisidan. Dexie'ning version(n).upgrade() lari va IndexedDB'ning oldVersion mantig'i — aynan shu. loadCart modul darajasida bir marta chaqirildi: renderda setItem — yon ta'sir bo'lardi (va StrictMode'da ikki marta).

4-mashq (tahlil): idb-keyval yoki Dexie?

O'lchov faylidagi ikki qatorga qarang: Dexie bitta vazifani yangilash — 1.9 ms, idb-keyval — 21.5 ms. Nega bunday farq va bu vazifalar uchun nega hal qiluvchi emas?

Yechim

idb-keyval ro'yxatni bitta kalitda saqlaydi: bitta vazifani o'zgartirish uchun butun 830 KB massivni o'qib, qayta yozish kerak. Dexie'da har vazifa — alohida yozuv: update(5000, …) faqat bittasiga tegadi. vazifalar da esa ro'yxat IndexedDB'da umuman yo'q — localStorage da, va u yerda ham butun massiv yoziladi (10 000 da 9.4 ms). Haqiqiy foydalanuvchida vazifalar yuzlab, minglab emas; 1 000 vazifada localStorage yozish 0.9 ms. Dexie'ning ustunligi faqat ko'p yozuvli va bittalab tez-tez yangilanadigan ma'lumotda seziladi.

8. Real ishda

  • Oflayn ilovalar (PWA): buyurtmalar, qoralamalar, keshlangan katalog — IndexedDB (ko'pincha Dexie), serverga sinxronlash navbati bilan. Servis ishchisi ham shu bazani o'qiydi.
  • Media: foydalanuvchi tanlagan rasm, ovozli xabar, eksport fayli — Blob sifatida idb-keyval'da.
  • TanStack Query keshini ham saqlash mumkin: persistQueryClient + IndexedDB'dagi saqlash joyi (Zustand persist ning createJSONStorage ga o'xshash). Server state oflaynda ham ko'rinadi.
  • Xavfsizlik: IndexedDB ham, localStorage ham shu saytdagi har skriptga ochiq — token va maxfiy ma'lumot u yerda emas (Frontend xavfsizligi).
  • Intervyu savollari: "localStorage va IndexedDB farqi?", "nega localStorage'ga rasm saqlamaysiz?", "IndexedDB sxemasini qanday o'zgartirasiz?", "boshqa tabdagi o'zgarishni qanday bilasiz?".

Xulosa

  • localStorage — sinxron, faqat matn, ~5 MB: JSON Date, Map, undefined ni yo'qotadi, rasm base64 bilan +33%, to'lganda QuotaExceededError.
  • sessionStorage — shu tab uchun; storage hodisasi — boshqa tablarga localStorage o'zgarishi haqida.
  • idb-keyval — IndexedDB'da kalit/qiymat, structured clone (Blob, Date, Map), React'ga useSyncExternalStore bilan.
  • Dexie — jadvallar, indekslar, where so'rovlar, version().upgrade() migratsiya, useLiveQuery — baza o'zgarsa ekran o'zi yangilanadi.
  • Sxema o'zgarsa — versiya oshadi va eski yozuvlar zinapoya bilan ko'chiriladi.
  • O'lchov (10 000 vazifa): localStorage o'qish 1.7 ms, idb-keyval 6.5, Dexie 135.8 — kanonda ro'yxat localStorage da, IndexedDB faqat profil rasmi uchun.

Keyingi dars: Holat mashinalari: XState va murakkab oqimlar — imkonsiz holatlarsiz ko'p bosqichli oqim: statechart, createMachine, useMachine, @xstate/store va vizualizator.

Manbalar

  • idb-keyval — github.com/jakearchibald/idb-keyval (v6.3)
  • Dexie.js: Version.upgrade(), WhereClause, useLiveQuery() — dexie.org/docs (v4, 2026-10 holatiga)
  • MDN: Storage quotas and eviction criteria; Structured clone algorithm; StorageEvent — developer.mozilla.org
  • web.dev: Storage for the web — web.dev/articles/storage-for-the-web
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
IndexedDB React'da: idb-keyval va Dexie bilan brauzerda ma'lumot saqlash — IlmHamroh