IlmHamroh
JavaScript Full-stack/18-qism. React chuqur35/42-dars24 daqiqa
Mundarija (34)

React + TS II: hooklar turlari — useState, useRef, useReducer va Context'da null tuzog'i

Qisqacha: Ko'p hooklar turini boshlang'ich qiymatdan o'zi chiqaradi: useState(0) — number. Muammo boshlang'ich qiymat "bo'sh" bo'lganda chiqadi: useState([]) — never[], useState(null) — faqat null. Bunday joyda turni o'zingiz yozasiz: useState<string[]>([]), useState<string | null>(null). DOM ref — useRef<HTMLInputElement>(null), uning current i doim null bo'lishi mumkin. Reducer amallari — discriminated union, Context esa createContext<T | null>(null) va null ni tekshiradigan guard hook bilan.

Bu darsda

  • useState turini qachon TypeScript o'zi chiqarishini va qachon <T> yozish shartligini ajrata olasiz.
  • Bir nechta boolean o'rniga union state yozasiz va imkonsiz holatlarni tur darajasida yopasiz.
  • useRef ning uch ko'rinishini (DOM, qiymat, React 19 dagi majburiy argument) to'g'ri turlaysiz.
  • Reducer amallarini discriminated union qilib, never bilan unutilgan amalni tsc'ga topdirasiz.
  • createContext<T | null> va guard hook yozasiz, vazifalar dagi useTema ni o'qiysiz.

Oldin bilishingiz kerak: React + TS I, useReducer, Context I, Discriminated union.

1. Nega bu kerak?

Oldingi darsda props'ga tur yozdik — komponentning "tashqi eshigi". Endi ichkariga kiramiz: komponent xotirasi, ya'ni hooklar.

Sardor buyurtmalar ro'yxatini yozdi. Ro'yxat bo'sh boshlanadi, keyin taomlar qo'shiladi:

tsx
const [items, setItems] = useState([]);
// …
setItems(["Osh"]);
// ❌ tsc: Type 'string' is not assignable to type 'never'

Xabar qo'rqitadi: never nima, nega satr unga to'g'ri kelmaydi? Sardor useState<any>([]) deb yozib, xatoni "yo'qotdi". Endi tsc umuman tekshirmaydi — ro'yxatga son ham, obyekt ham tushadi.

Hayotiy o'xshatish: oshxonadagi bo'sh idish. Idishga yorliq yopishtirilmasa, oshpaz unga nima solish mumkinligini bilmaydi. TypeScript ham bo'sh massivga qarab "bu yerga hech narsa tushmaydi" (never) deb xulosa qiladi. Yechim — idishga oldindan yorliq yopishtirish: useState<string[]>([]). Bu darsda hooklarga yorliqni qachon va qanday yopishtirishni ko'ramiz.

2. useState turlari

2.1 Tur chiqarish (inference) — ko'p hollarda yetarli

Boshlang'ich qiymat "to'liq" bo'lsa, <T> yozish shart emas:

tsx
import { useState } from "react";

export default function App() {
  const [count, setCount] = useState(0); // number
  const [dish, setDish] = useState("Osh"); // string
  const [open, setOpen] = useState(false); // boolean
  return (
    <div>
      <p>
        {dish} × {count} {open ? "(ochiq)" : "(yopiq)"}
      </p>
      <button onClick={() => setCount((c) => c + 1)}>+</button>{" "}
      <button onClick={() => setDish("Manti")}>Manti</button>{" "}
      <button onClick={() => setOpen((o) => !o)}>Ochish</button>
    </div>
  );
}

Ekranda:

text
Osh × 0 (yopiq)
+ Manti Ochish

Muharrirda count ustiga sichqonchani olib borsangiz — number. setCount turi esa Dispatch<SetStateAction<number>>. Uzun nom, lekin ma'nosi oddiy. SetStateAction<number> — "yo son, yo (oldingi) => son funksiyasi" (snapshot va updater). Dispatch esa "shuni qabul qilib, hech narsa qaytarmaydigan funksiya".

2.2 Bo'sh boshlanish: [] va null

Ikki tuzoq — ikkalasi ham "bo'sh" boshlang'ich qiymatdan:

tsx
import { useState } from "react";

export default function App() {
  const [items, setItems] = useState([]);
  const [error, setError] = useState(null);
  function add() {
    setItems(["Osh"]);
    setError("Stol tanlanmagan");
  }
  return <button onClick={add}>Qo'shish</button>;
}

Xato:

text
TS2322: Type 'string' is not assignable to type 'never'.
TS2345: Argument of type '"Stol tanlanmagan"' is not assignable to parameter of type 'SetStateAction<null>'.
  1. "string ni never ga berib bo'lmaydi." [] dan TypeScript never[] chiqardi — "hech qanday element bo'lmaydigan massiv". never — hech qachon bo'lmaydigan qiymat turi.
  2. ""Stol tanlanmagan" ni SetStateAction<null> ga berib bo'lmaydi." null dan tur — faqat null. Bu state hech qachon boshqa narsa bo'la olmaydi.

Tuzatish — idishga yorliq:

tsx
import { useState } from "react";

export default function App() {
  const [items, setItems] = useState<string[]>([]);
  const [error, setError] = useState<string | null>(null);
  function add() {
    if (items.length >= 2) {
      setError("Ko'pi bilan 2 ta taom");
      return;
    }
    setItems([...items, "Osh"]);
  }
  return (
    <div>
      <button onClick={add}>Qo'shish</button>
      <p>{items.join(", ") || "Bo'sh"}</p>
      {error !== null && <p role="alert">{error}</p>}
    </div>
  );
}

Ekranda:

text
Qo'shish
Bo'sh

Uch marta bosib ko'ring: ikki "Osh" qo'shiladi, uchinchisida xato chiqadi. error !== null && — tekshiruvdan keyin error turi string, null emas (Narrowing). any ga qochish shart bo'lmadi.

2.3 Lazy init va tur

Boshlang'ich qiymat funksiyadan kelsa (dangasa boshlash), tur funksiyaning qaytish turidan chiqadi:

tsx
const [draft] = useState(() => localStorage.getItem("qoralama"));
// draft: string | null — getItem shunday qaytaradi

localStorage.getItem — string | null, chunki kalit bo'lmasligi mumkin. Tur buni eslatib turadi: draft.length yozsangiz — 'draft' is possibly 'null'. vazifalar dagi TemaProvider ham shunday: useState(saqlanganTanlov) — funksiya, tur esa uning natijasidan.

Tekshirib ko'ring: const [price, setPrice] = useState<number>() yozildi. price turi qanday bo'ladi va nega?

Javob

number | undefined. Argument berilmagan — boshlang'ich qiymat undefined. @types/react da buning uchun alohida yozuv (overload) bor: argumentsiz useState<S>() turni S | undefined qiladi. Shuning uchun price.toFixed() — xato, avval price !== undefined tekshiruvi kerak. Agar qiymat boshidan ma'lum bo'lsa, useState(0) yozing — undefined holati umuman yo'qoladi.

3. Union state: imkonsiz holatlarni yopish

3.1 Uch boolean — sakkiz holat

Taomlar ro'yxatini yuklash. Birinchi urinish ko'pincha shunday bo'ladi:

tsx
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [dishes, setDishes] = useState<string[] | null>(null);

Uch mustaqil state. Ular ichida "yuklanmoqda VA xato VA ma'lumot bor" kabi bema'ni kombinatsiya ham bor. Holat mashinasi darsida buni "imkonsiz holat" degan edik. U yerda bitta status satriga o'tgan edik. TypeScript bu fikrni yana bir qadam oldinga suradi: har holat o'zi bilan birga kerakli ma'lumotni olib yuradi.

3.2 Discriminated union state

tsx
import { useState } from "react";

type Load =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "ok"; dishes: string[] }
  | { status: "error"; message: string };

export default function App() {
  const [load, setLoad] = useState<Load>({ status: "idle" });

  function fetchMenu() {
    setLoad({ status: "loading" });
    setTimeout(() => {
      setLoad({ status: "ok", dishes: ["Osh", "Manti"] });
    }, 200);
  }

  return (
    <div>
      <button onClick={fetchMenu}>Menyuni yuklash</button>
      {load.status === "loading" && <p>Yuklanmoqda…</p>}
      {load.status === "ok" && <p>{load.dishes.join(", ")}</p>}
      {load.status === "error" && <p role="alert">{load.message}</p>}
    </div>
  );
}

Ekranda:

text
Menyuni yuklash

Tugmani bosgandan keyin "Yuklanmoqda…", 200 ms dan keyin "Osh, Manti" chiqadi. Asosiy narsa — JSX ichidagi tekshiruvlar. load.status === "ok" dan keyin TypeScript load ning qaysi varianti ekanini biladi va load.dishes ga ruxsat beradi. "error" holatida esa dishes umuman yo'q:

tsx
type Load =
  | { status: "loading" }
  | { status: "ok"; dishes: string[] }
  | { status: "error"; message: string };

function MenuView({ load }: { load: Load }) {
  if (load.status === "error") {
    return <p>{load.dishes.length} ta taom</p>;
  }
  return <p>{load.status}</p>;
}

export default function App() {
  return <MenuView load={{ status: "loading" }} />;
}

Xato:

text
TS2339: Property 'dishes' does not exist on type '{ status: "error"; message: string; }'.

"dishes xususiyati { status: "error"; message: string } turida yo'q." Uchta mustaqil state bilan bu xato faqat ish vaqtida, undefined.length bo'lib chiqardi. Holatlar va o'tishlarni diagrammada ko'ring:

stateDiagram-v2
  [*] --> idle
  idle --> loading: tugma
  loading --> ok: javob keldi
  loading --> error: server xatosi
  error --> loading: qayta urinish
  ok --> loading: yangilash

Har holat — union'ning bitta varianti. ok da dishes bor, error da message bor, boshqalarida yo'q. Imkonsiz kombinatsiya yozib bo'lmaydi: { status: "loading", message: "…" } ham tsc xatosi.

4. useRef turlari

4.1 DOM ref: useRef<HTMLInputElement>(null)

useRef II darsida DOM elementga ref ulagan edik. Turi shunday yoziladi:

tsx
import { useRef } from "react";

export default function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  function focusInput() {
    inputRef.current.focus();
  }
  return (
    <div>
      <input ref={inputRef} aria-label="Taom" />
      <button onClick={focusInput}>Yozish</button>
    </div>
  );
}

Xato:

text
TS18047: 'inputRef.current' is possibly 'null'.

"inputRef.current null bo'lishi mumkin." To'g'ri ogohlantirish: birinchi render paytida element hali yo'q, unmount'dan keyin esa yana null. Ikki tuzatish bor:

tsx
import { useRef } from "react";

export default function App() {
  const inputRef = useRef<HTMLInputElement>(null);
  function focusInput() {
    inputRef.current?.focus(); // null bo'lsa — hech narsa
    console.log("fokus:", document.activeElement?.id);
  }
  return (
    <div>
      <input id="taom" ref={inputRef} aria-label="Taom" />
      <button onClick={focusInput}>Yozish</button>
    </div>
  );
}

Konsolda:

text
fokus: taom

Birinchi tuzatish — ?. (optional chaining) — eng qisqa yo'l. Ikkinchi yo'l — if (inputRef.current === null) return; va undan keyin bemalol ishlatish. inputRef.current!.focus() (non-null !) ham ishlaydi, lekin u tsc'ga "menga ishon" deydi va ogohlantirishni o'chiradi. Bu assertion — xato bo'lsa, ish vaqtida yiqiladi.

Tur nomlari — brauzerdagi DOM sinflari: HTMLInputElement, HTMLButtonElement, HTMLDivElement, HTMLHeadingElement, HTMLDialogElement. Noto'g'ri tur bersangiz (useRef<HTMLDivElement> ni <input ref> ga) — tsc xato beradi.

4.2 Qiymat ref: useRef(0)

Ref DOM uchun emas, render chaqirmaydigan xotira uchun bo'lsa — boshlang'ich qiymatdan tur chiqadi:

tsx
import { useRef } from "react";

export default function App() {
  const clicks = useRef(0); // RefObject<number>
  function handleClick() {
    clicks.current++;
    console.log("bosishlar:", clicks.current);
  }
  return <button onClick={handleClick}>Bosish</button>;
}

Konsolda:

text
bosishlar: 1
bosishlar: 2
bosishlar: 3

Ekranda:

text
Bosish

Taymer id'si uchun: useRef<number | null>(null) — setTimeout brauzerda son qaytaradi, boshida esa taymer yo'q.

4.3 React 19: argumentsiz useRef() — xato

Eski kodda useRef<HTMLInputElement>() ko'p uchraydi. @types/react 19 da argument majburiy:

tsx
import { useRef } from "react";

export default function App() {
  const ref = useRef<HTMLInputElement>();
  return <input ref={ref} aria-label="Taom" />;
}

Xato:

text
TS2554: Expected 1 arguments, but got 0.

"1 ta argument kutilgan, 0 ta berildi." React 19 gacha argumentsiz chaqiruv undefined bilan boshlanardi. Endi turlar bunga yo'l qo'ymaydi: React DOM ref'ni null bilan to'ldiradi, undefined bilan emas. Davosi bitta — useRef<HTMLInputElement>(null). React 18 → 19 ga ko'chishda bu eng ko'p uchraydigan tur xatolaridan biri (React 18 dan 19 ga).

Yana bir o'zgarish: React 19 turlarida RefObject ning current i o'zgartirsa bo'ladigan. Eski MutableRefObject endi kerak emas. Eski kodda ko'rsangiz — shunchaki RefObject ning eski nomi.

Maqsad Yozuv current turi
DOM element useRef<HTMLInputElement>(null) HTMLInputElement | null
oddiy qiymat useRef(0) number
keyin to'ladigan qiymat useRef<number | null>(null) number | null

5. useReducer: amallar — discriminated union

5.1 Amal turi

useReducer darsida amallar { tur: "…" } obyektlari edi. TypeScript'da ularning hammasi bitta union'ga yig'iladi:

tsx
import { useReducer } from "react";

type Action =
  | { type: "add"; dish: string }
  | { type: "remove"; dish: string }
  | { type: "clear" };

function orderReducer(state: string[], action: Action): string[] {
  switch (action.type) {
    case "add":
      return [...state, action.dish];
    case "remove":
      return state.filter((d) => d !== action.dish);
    case "clear":
      return [];
    default: {
      const unhandled: never = action;
      return unhandled;
    }
  }
}

export default function App() {
  const [order, dispatch] = useReducer(orderReducer, []);
  return (
    <div>
      <button onClick={() => dispatch({ type: "add", dish: "Osh" })}>
        Osh
      </button>{" "}
      <button onClick={() => dispatch({ type: "clear" })}>
        Tozalash
      </button>
      <p>{order.length} ta taom</p>
    </div>
  );
}

Ekranda:

text
Osh Tozalash
0 ta taom

Uch narsa avtomatik ishlaydi:

  • useReducer(orderReducer, []) — <T> yozilmadi. State va amal turi reducer funksiyasining o'zidan olindi. Bo'sh [] bu yerda tuzoq emas: reducer state: string[] deb aytdi.
  • case "add": ichida action.dish — xatosiz, chunki bu tarmoqda action faqat { type: "add"; dish: string }.
  • dispatch ham turlangan: noto'g'ri amal yuborib bo'lmaydi.

5.2 Noto'g'ri amal va unutilgan case

tsx
import { useReducer } from "react";

type Action = { type: "add"; dish: string } | { type: "clear" };

function orderReducer(state: string[], action: Action): string[] {
  switch (action.type) {
    case "add":
      return [...state, action.dish];
    default: {
      const unhandled: never = action;
      return unhandled;
    }
  }
}

export default function App() {
  const [order, dispatch] = useReducer(orderReducer, []);
  dispatch({ type: "add" });
  dispatch({ type: "remove", dish: "Osh" });
  return <p>{order.length}</p>;
}

Xato:

text
TS2322: Type '{ type: "clear"; }' is not assignable to type 'never'.
TS2345: Argument of type '{ type: "add"; }' is not assignable to parameter of type 'Action'.
  Property 'dish' is missing in type '{ type: "add"; }' but required in type '{ type: "add"; dish: string; }'.
TS2322: Type '"remove"' is not assignable to type '"add" | "clear"'.
  1. "{ type: "clear" } ni never ga berib bo'lmaydi" — case "clear" unutilgan. default ga yetib kelgan amal never bo'lishi kerak edi, lekin clear qolib ketdi. Bu — to'liqlik tekshiruvi.
  2. "add amalida dish yo'q" — yarim amal.
  3. ""remove" turi "add" | "clear" ga kirmaydi" — mavjud bo'lmagan amal.

Ikkinchi va uchinchi xato dispatch ni chaqirgan joyda — komponentda. Birinchisi reducer ichida. Ya'ni yangi amal turini qo'shsangiz, tsc sizni reducer'ga yuboradi: "buni ishlamading".

Tekshirib ko'ring: default ichida const unhandled: never = action; o'rniga return state; yozilsa, nima yo'qoladi?

Javob

Kod ishlaydi, lekin to'liqlik tekshiruvi yo'qoladi. Yangi amal ("remove") qo'shilib, case yozish unutilsa — tsc jim. Amal yuboriladi, reducer uni default da "yutib yuboradi", ekranda hech narsa o'zgarmaydi. never qatori shu jimlikni kompilyatsiya xatosiga aylantiradi.

6. Context: createContext<T | null> va guard hook

6.1 Muammo: standart qiymat

createContext bitta argument talab qiladi — Provider topilmasa ishlatiladigan standart qiymat. Murakkab qiymat uchun ({ tanlov, tanla }) "soxta" standart yasash yomon fikr: Provider unutilsa, xato jim yashirinadi. Shuning uchun standart — null, tur esa T | null:

tsx
import { createContext, use } from "react";

type Cart = { count: number; add: () => void };

const CartContext = createContext<Cart | null>(null);

function CartBadge() {
  const cart = use(CartContext);
  return <b>{cart.count}</b>;
}

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

Xato:

text
TS18047: 'cart' is possibly 'null'.

TypeScript haq: App da Provider umuman yo'q. Har iste'molchida if (cart === null) yozish esa zerikarli va takrorlanadi.

6.2 Guard hook — tekshiruv bir joyda

Yechim — custom hook, u null ni bir marta tekshiradi va aniq xato tashlaydi:

tsx
import { createContext, use, useState } from "react";
import type { ReactNode } from "react";

type Cart = { count: number; add: () => void };

const CartContext = createContext<Cart | null>(null);

function CartProvider({ children }: { children: ReactNode }) {
  const [count, setCount] = useState(0);
  const value: Cart = { count, add: () => setCount((c) => c + 1) };
  return <CartContext value={value}>{children}</CartContext>;
}

function useCart(): Cart {
  const cart = use(CartContext);
  if (cart === null) {
    throw new Error("useCart() — <CartProvider> ichida chaqirilsin");
  }
  return cart;
}

function AddButton() {
  const { add } = useCart();
  return <button onClick={add}>Savatga</button>;
}

function CartBadge() {
  const { count } = useCart();
  return <b> Savatda: {count}</b>;
}

export default function App() {
  return (
    <CartProvider>
      <AddButton />
      <CartBadge />
    </CartProvider>
  );
}

Ekranda:

text
Savatga Savatda: 0

useCart() qaytish turi — Cart, null emas. Iste'molchilar toza: const { add } = useCart(). Provider unutilsa — konsolda (Vite dev serverida esa sahifa ustidagi xato oynasida ham) aniq gap chiqadi: useCart() — <CartProvider> ichida chaqirilsin. Jim undefined emas.

Provider'siz holat ham shunday ko'rinadi:

tsx
import { createContext, use } from "react";

type Cart = { count: number };
const CartContext = createContext<Cart | null>(null);

function useCart(): Cart {
  const cart = use(CartContext);
  if (cart === null) {
    throw new Error("useCart() — <CartProvider> ichida chaqirilsin");
  }
  return cart;
}

function CartBadge() {
  return <b>{useCart().count}</b>;
}

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

Konsolda:

text
Uncaught Error: useCart() — <CartProvider> ichida chaqirilsin

Xato dasturchiga, ishlab chiqish paytida chiqadi — ilova foydalanuvchiga yetib bormaydi. Bu yaxshi xato: nima qilish kerakligi matnda yozilgan.

6.3 Qachon null siz?

Context qiymati oddiy va standart qiymati ma'noli bo'lsa, null shart emas: createContext<"uz" | "ru">("uz") — Provider'siz ham til bor. Guard hook — Provider majburiy bo'lgan holat uchun: savat, auth, tema boshqaruvi. 42-darsdagi useAuth ham aynan shu shaklda bo'ladi (React'da auth holati).

7. Qolgan hooklar: tur qayerdan keladi

7.1 useMemo va useCallback

useMemo turi — funksiya nima qaytarsa, o'sha. Odatda hech narsa yozish shart emas. useCallback da esa bitta tuzoq bor: funksiya parametrlari turi chiqarilmaydi, chunki useCallback uni qayerga uzatishingizni bilmaydi:

tsx
import { useCallback, useMemo } from "react";

export default function App() {
  const dishes = ["Osh", "Manti"];
  const count = useMemo(() => dishes.length, [dishes]); // number
  const pick = useCallback((dish) => console.log(dish), []);
  return <button onClick={() => pick("Osh")}>{count}</button>;
}

Xato:

text
TS7006: Parameter 'dish' implicitly has an 'any' type.

"dish parametri yashirincha any turini oldi." strict rejimda bu taqiqlangan. Tuzatish — parametrga tur: useCallback((dish: string) => …, []). Xuddi shu funksiya to'g'ridan-to'g'ri onClick={(e) => …} ga yozilsa, tur kerak emas edi: u yerda TypeScript kutilgan turni biladi. Bu "kontekstli tur" — keyingi darsda hodisalarda ko'p uchraydi.

7.2 useActionState va useOptimistic

18-qismdagi yangi hooklar ham turni funksiyadan oladi. useActionState — action funksiyasining (oldingi, FormData) => Promise<Holat> turidan:

tsx
type FormState = { error: string | null };

async function saveDish(
  prev: FormState,
  data: FormData,
): Promise<FormState> {
  const name = data.get("dish"); // FormDataEntryValue | null
  if (typeof name !== "string" || name === "") {
    return { error: "Taom nomi kerak" };
  }
  return { error: null };
}

const [state, formAction, isPending] = useActionState(saveDish, {
  error: null,
});
// state: FormState, formAction: (payload: FormData) => void,
// isPending: boolean

Ikki nozik joy. Birinchisi — data.get("dish") turi FormDataEntryValue | null, ya'ni satr, fayl yoki null. Shuning uchun typeof name !== "string" tekshiruvi shart. vazifalar dagi formalar ham shunday qiladi. Ikkinchisi — boshlang'ich qiymat { error: null } dan tur chiqarilmaydi, u action funksiyasidan olinadi. Aks holda yana null tuzog'i bo'lardi.

useOptimistic da tur — birinchi argumentdan (asosiy holat) va reducer'ning ikkinchi parametridan:

tsx
const [shown, addOptimistic] = useOptimistic(
  dishes, // string[]
  (current, dish: string) => [...current, dish],
);
// addOptimistic: (action: string) => void

dish: string — yozilishi shart, useCallback dagi sabab bilan. current ning turi esa dishes dan keladi.

Hook Tur qayerdan Yozish kerakmi
useMemo funksiya natijasi yo'q
useCallback funksiyaning o'zi parametr turlari — ha
useActionState action funksiyasi parametr va qaytish turi
useOptimistic asosiy holat + reducer reducer'ning 2-parametri
useTransition — yo'q: [boolean, startTransition]

8. Custom hook qaytish turi

Custom hook massiv qaytarsa, TypeScript uni tuple deb emas, oddiy massiv deb chiqaradi:

tsx
import { useState } from "react";

function useToggle(start: boolean) {
  const [on, setOn] = useState(start);
  const toggle = () => setOn((v) => !v);
  return [on, toggle];
}

export default function App() {
  const [open, toggle] = useToggle(false);
  const label = open ? "Yopish" : "Ochish";
  return <button onClick={toggle}>{label}</button>;
}

Xato:

text
TS2322: Type 'boolean | (() => void)' is not assignable to type 'MouseEventHandler<HTMLButtonElement> | undefined'.
  Type 'boolean' is not assignable to type 'MouseEventHandler<HTMLButtonElement>'.

Qaytish turi — (boolean | (() => void))[]: "har element yo boolean, yo funksiya". toggle ham shunday aralash bo'lib qoldi. Ikki tuzatish:

tsx
import { useState } from "react";

function useToggle(start: boolean) {
  const [on, setOn] = useState(start);
  const toggle = () => setOn((v) => !v);
  return [on, toggle] as const; // readonly [boolean, () => void]
}

export default function App() {
  const [open, toggle] = useToggle(false);
  const label = open ? "Yopish" : "Ochish";
  return <button onClick={toggle}>{label}</button>;
}

Ekranda:

text
Ochish

as const — "bu aynan ikki elementli, tartibi qat'iy tuple". Ikkinchi yo'l — qaytish turini ochiq yozish: function useToggle(start: boolean): [boolean, () => void]. vazifalar dagi useBoshqariladiganHolat ikkinchi yo'lni tanlagan. Ikkitadan ko'p qiymat bo'lsa — massiv emas, obyekt qaytaring: { open, toggle, close } — nomlar bilan.

9. Vazifalar qadami: kanondagi hook turlarini o'qish

refactor/turlar branch'ida bugun kod o'zgarmaydi — bu darsdagi qoidalar vazifalar da 17-qismdan beri bor. Ularni topib o'qiymiz. Uchta fayl.

kontekst/TemaKonteksti.tsx — guard hook, kanondan aynan:

tsx
type TemaQiymati = {
  tanlov: Tema;
  tanla: (yangi: Tema) => void;
};

// null — Provider yo'q (dasturchi xatosi): useTema() aniq xato beradi
const TemaKonteksti = createContext<TemaQiymati | null>(null);

/** Tema konteksti. Provider ichida bo'lmasa — aniq xato
    (null emas) */
export function useTema(): TemaQiymati {
  // use() — useContext kabi, lekin shart ichida ham chaqirsa bo'ladi
  const qiymat = use(TemaKonteksti);
  if (qiymat === null) {
    throw new Error("useTema() — <TemaProvider> ichida chaqirilsin");
  }
  return qiymat;
}

Xuddi «Guard hook — tekshiruv bir joyda» bo'limidagi useCart. Ikkinchi nusxasi — kontekst/qator-amallari.ts dagi useQatorAmallari.

reducer.ts — amallar union'i (qisqartirilgan, kanonda 8 variant) va never:

ts
export type VazifaAmali =
  | { readonly tur: "qoshildi"; readonly matn: string }
  | { readonly tur: "almashtirildi"; readonly id: number }
  | { readonly tur: "ochirildi"; readonly id: number }
  // … matnOzgardi, serverdanKeldi, royxatKeldi, tahrirBoshlandi
  | { readonly tur: "tahrirYopildi" };

    default: {
      const qolmadi: never = amal;

Amal nomlari — o'tgan zamonda ("qo'shildi", "o'chirildi"): amal "nima qil" buyrug'i emas, "nima bo'ldi" xabari. Har xususiyat readonly — reducer amalni o'zgartirmaydi.

ui/useMenyu.ts — ref va union state:

ts
type FokusJoyi = "birinchi" | "oxirgi";

export type MenyuBoshqaruvi = ReturnType<typeof useMenyu>;

  const tugmaRef = useRef<HTMLButtonElement>(null);
  const royxatRef = useRef<HTMLDivElement>(null);
  const [fokusJoyi, setFokusJoyi] = useState<FokusJoyi>("birinchi");

useState<FokusJoyi> — <T> shart: usiz "birinchi" dan tur string chiqardi va setFokusJoyi("pastki") o'tib ketardi. MenyuBoshqaruvi esa hook qaytaradigan obyekt turini qo'lda yozmasdan oladi: ReturnType "funksiya nima qaytarsa — shu tur". Compound Menyu.tsx uni Context turi sifatida ishlatadi.

Tekshirish — npm run tip: 0 xato. Bugun commit yo'q.

10. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
Type 'string' is not assignable to type 'never' useState([]) useState<string[]>([])
SetStateAction<null> xatosi useState(null) useState<T | null>(null)
'x.current' is possibly 'null' DOM ref ?., if tekshiruvi
TS2554: Expected 1 arguments useRef<T>() (React 19) useRef<T>(null)
Type '{…}' is not assignable to type 'never' reducer'da case unutilgan case qo'shing
'ctx' is possibly 'null' createContext<T | null> guard hook
boolean | (() => void) hook massiv qaytaradi as const yoki qaytish turi

11. Mashqlar

Mashqlar kurs/mashqlar/18/35-hooklar-turlari/ da.

1-mashq (oson): turni tanlang

Har state uchun useState qatorini to'ldiring.

  • Tanlangan stol raqami, boshida tanlanmagan: useState<>(null)
  • Savatdagi taomlar nomlari, boshida bo'sh: useState<]>([])
  • Yetkazish usuli — faqat "olib ketish" yoki "yetkazish", boshida olib ketish: useState<>("olib ketish")
Yechim
  • number | null — "tanlanmagan" holati null, keyin son. 0 bilan boshlash xato bo'lardi: 0-stol yo'q, lekin tur buni farqlay olmaydi.
  • string[] — bo'sh massivga yorliq, aks holda never[].
  • "olib ketish" | "yetkazish" — literal union. Usiz "olib ketish" dan string chiqadi va istalgan satr o'tadi.

2-mashq (o'rta): union state

Ikki tugmali buyurtma: «Yuborish» va «Xato bilan yuborish». State — type Send = { status: "idle" } | { status: "sent"; id: number } | { status: "failed"; reason: string }. «Yuborish» — sent, id 101; «Xato bilan yuborish» — failed, sabab "Internet yo'q". Ekranda: sent — Buyurtma #101 qabul qilindi, failed — role="alert" da sabab, idle — hech narsa. Ishora: «Discriminated union state» bo'limidagi load.status === … tekshiruvlari.

Yechim
tsx
import { useState } from "react";

type Send =
  | { status: "idle" }
  | { status: "sent"; id: number }
  | { status: "failed"; reason: string };

export default function App() {
  const [send, setSend] = useState<Send>({ status: "idle" });
  return (
    <div>
      <button onClick={() => setSend({ status: "sent", id: 101 })}>
        Yuborish
      </button>{" "}
      <button
        onClick={() =>
          setSend({ status: "failed", reason: "Internet yo'q" })
        }
      >
        Xato bilan yuborish
      </button>
      {send.status === "sent" && (
        <p>Buyurtma #{send.id} qabul qilindi</p>
      )}
      {send.status === "failed" && (
        <p role="alert">{send.reason}</p>
      )}
    </div>
  );
}

Ekranda:

text
Yuborish Xato bilan yuborish
Internet yo'q

send.id faqat status === "sent" tarmog'ida o'qiladi — boshqa joyda yozsangiz, TS2339. useState<Send> dagi <Send> shart: usiz { status: "idle" } dan tur { status: string } chiqardi va setSend({ status: "sent", id: 101 }) xato berardi.

3-mashq (qiyin): reducer va guard hook

Stol bron qilish konteksti yozing:

  1. type Action = { type: "book"; table: number } | { type: "cancel" }, state — number | null (bron qilingan stol).
  2. Reducer'da never bilan to'liqlik tekshiruvi.
  3. BookingContext — createContext<… | null>(null), qiymati { table, dispatch }. Guard hook — useBooking().
  4. App da Provider ichida ikki komponent: BookButton (bosilsa — 5-stol) va Status (5-stol bron qilindi yoki Bron yo'q).

Ishora: dispatch turi — useReducer dan keladi, uni Dispatch<Action> deb yozish mumkin (import type { Dispatch } from "react").

Yechim
tsx
import { createContext, use, useReducer } from "react";
import type { Dispatch, ReactNode } from "react";

type Action = { type: "book"; table: number } | { type: "cancel" };

function bookingReducer(state: number | null, action: Action) {
  switch (action.type) {
    case "book":
      return action.table;
    case "cancel":
      return null;
    default: {
      const unhandled: never = action;
      return unhandled;
    }
  }
}

type Booking = { table: number | null; dispatch: Dispatch<Action> };

const BookingContext = createContext<Booking | null>(null);

function useBooking(): Booking {
  const booking = use(BookingContext);
  if (booking === null) {
    throw new Error("useBooking() — <BookingProvider> ichida");
  }
  return booking;
}

function BookingProvider({ children }: { children: ReactNode }) {
  const [table, dispatch] = useReducer(bookingReducer, null);
  return (
    <BookingContext value={{ table, dispatch }}>
      {children}
    </BookingContext>
  );
}

function BookButton() {
  const { dispatch } = useBooking();
  return (
    <button onClick={() => dispatch({ type: "book", table: 5 })}>
      5-stolni bron qilish
    </button>
  );
}

function Status() {
  const { table } = useBooking();
  const text =
    table === null ? "Bron yo'q" : `${table}-stol bron qilindi`;
  return <p>{text}</p>;
}

export default function App() {
  return (
    <BookingProvider>
      <BookButton />
      <Status />
    </BookingProvider>
  );
}

Ekranda:

text
5-stolni bron qilish
5-stol bron qilindi

Reducer'ning qaytish turi yozilmagan — tsc uni number | null deb chiqardi (action.table — son, null, never). useReducer(bookingReducer, null) da null ham tuzoq emas: state turi reducer'dan olinadi. Dispatch<Action> — "Action qabul qilib, hech narsa qaytarmaydigan funksiya", aynan useReducer bergan dispatch.

4-mashq: Vazifalar qadami

Loyihada uchta joyni toping va har biriga bir gap yozing:

  1. createContext<… | null> va uning guard hook'i (ikkita bor).
  2. useState ga <T> ochiq berilgan joy va nega shart.
  3. never bilan to'liqlik tekshiruvi (reducer.ts dan tashqari yana bitta bor).
Yechim
  • TemaKonteksti.tsx → useTema(); qator-amallari.ts → useQatorAmallari().
  • useMenyu.ts: useState<FokusJoyi>("birinchi") — usiz tur string bo'lardi. Boshqa misol: forma xatosi useState<string | null>(null) (17-qism).
  • reducer.ts dagi const qolmadi: never = amal; va App.tsx dagi boshqa tab xabarlari switch'i (SinxronXabar). Ikkinchisi 42-darsda yangi xabar turi qo'shilganda haqiqatan ishga tushadi.
  • npm run tip — 0 xato, commit yo'q.

12. Real ishda

  • Union state — so'rov holati uchun eng keng tarqalgan naqsh. TanStack Query ham natijani shunday beradi: status: "pending" | "error" | "success" (TanStack Query I).
  • Guard hook — deyarli har kutubxonada: useRouter, useForm, useTheme Provider'siz chaqirilsa aniq xato beradi.
  • never bilan to'liqlik — Redux Toolkit, XState va reducer'li har loyihada. Kod ko'rigida default: return state ko'rilsa — "never tekshiruvi qani?" degan savol tug'iladi.
  • Intervyu savollari: "useState([]) nega never[]?", "useRef<T>(null) va useRef<T>() farqi?", "Context'ga null standart qiymat nega?".

Xulosa

  • useState turi boshlang'ich qiymatdan chiqadi; bo'sh [] va null da — <T> ochiq: useState<string[]>([]), useState<T | null>(null).
  • Bir nechta boolean o'rniga — discriminated union state: imkonsiz holatni yozib bo'lmaydi.
  • DOM ref — useRef<HTMLInputElement>(null), current — T | null; React 19 da argument majburiy.
  • Reducer amallari — union, default da never — unutilgan case tsc xatosi.
  • Context — createContext<T | null>(null) + guard hook; hook massiv qaytarsa — as const.
  • vazifalar: useTema, VazifaAmali va useMenyu — shu qoidalar 17-qismdan beri; bugun kod o'zgarmadi.

Keyingi dars: React + TS III: hodisalar va HTML atributlari — hodisa turlari va native element props'larini meros qilib olish.

Manbalar

  • React: "Using TypeScript — Typing Hooks" — react.dev/learn/typescript (2026-10 holatiga, React 19.3)
  • React 19 Upgrade Guide: "useRef requires an argument" — react.dev/blog/2024/04/25/react-19-upgrade-guide
  • TypeScript: Narrowing — Discriminated unions, never — typescriptlang.org/docs/handbook/2/narrowing.html
  • @types/react 19.3.0 — useState, useRef, useReducer, createContext ta'riflari
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React + TS II: hooklar turlari — useState, useRef, useReducer va Context'da null tuzog'i — IlmHamroh