Բոլոր դասերը
FrontendՄիջին14 րոպե ընթերցումԻմ ստեկում

React

Ինտերֆեյսներ կառուցիր կոմպոնենտներից՝ state, hook-եր, rendering և React 19-ի server հնարավորությունները։

  • React 19
  • Hooks
  • State
  • Server Components
  • Zustand
Քայլ 1/5
React հավելվածը կոմպոնենտների ծառ է՝ վերևում App-ն է, տակը՝ Header-ն ու Cart-ը, և ամեն մեկն ունի իր երեխաները։

30 վայրկյանում

React-ը JavaScript գրադարան է՝ օգտատիրոջ ինտերֆեյսներ կառուցելու համար փոքր, վերաօգտագործվող մասերից, որոնք կոչվում են կոմպոնենտներ։ Դու նկարագրում ես, թե ինչպիսին պետք է լինի էկրանը տվյալ state-ի համար, և React-ը արդյունավետ թարմացնում է էջը, երբ այդ state-ը փոխվում է։ React 19-ը ավելացնում է Action-ներ ձևերի համար, use hook-ը և Server Component-ներ, որոնք render են լինում սերվերում և բրաուզերին ավելի քիչ JavaScript են ուղարկում։

Ինչու եմ օգտագործում

  • React օգտագործում եմ իմ կառուցած գրեթե յուրաքանչյուր ինտերֆեյսի համար՝ Next.js խանութներ, ադմին պանելներ, Electron դրամարկղ և React Native-ի միջոցով՝ մոբայլ հավելվածներ։
  • Ինձ դուր է գալիս, որ կոմպոնենտային մոդելը scale է անում. նույն pattern-ներն աշխատում են և՛ մեկ կոճակի, և՛ տասնյակ էկրաններով ադմին համակարգի համար։
  • Սպասարկում եմ վերաօգտագործվող React փաթեթներ, այդ թվում config-driven ադմին համակարգ և account ու cart մոդուլներ, որոնք կիսվում են բազմաթիվ հաճախորդների կայքերի միջև։
  • State-ի գործիքներն ընտրում եմ գիտակցաբար. նախ լոկալ state, Zustand՝ client state-ի համար, և server-cache գրադարան, ինչպես TanStack Query-ն, API տվյալների համար։

Ինչպես է աշխատում

01Կոմպոնենտներ, props և state

Կոմպոնենտը ֆունկցիա է, որը վերադարձնում է JSX՝ սինտաքս, որը նման է HTML-ի և նկարագրում է UI։ Կոմպոնենտները միավորվում են խորանարդիկների պես. ProductCard-ը ProductGrid-ի ներսում, որն էլ ShopPage-ի ներսում։ Props-ը այն մուտքերն են, որոնք ծնողը փոխանցում է երեխային. երեխայի տեսանկյունից դրանք միայն կարդալու համար են, ինչպես ֆունկցիայի արգումենտները։ State-ը տվյալներ են, որոնք կոմպոնենտը տիրապետում է և կարող է փոխել, օրինակ՝ արդյոք մենյուն բաց է, կամ ինչ է մուտքագրված դաշտում։ Երբ state-ը փոխվում է, React-ը re-render է անում այդ կոմպոնենտը և նրա երեխաներին՝ նոր արժեքներով։ Հիմնական գաղափարը «UI-ը state-ի ֆունկցիա է» սկզբունքն է. երբեք DOM-ը ձեռքով չես փոփոխում, փոխում ես state-ը և նկարագրում արդյունքը։ Տվյալները հոսում են ներքև props-ի միջոցով, իսկ փոփոխությունները՝ վեր՝ callback props-ի միջոցով, ինչպես onChange-ը։ Երբ երկու կոմպոնենտի պետք է նույն state-ը, բարձրացրու այն նրանց ամենամոտ ընդհանուր ծնողի մոտ։

tsx
type Product = { id: string; name: string; price: number };
function ProductCard({ product, onAdd }: { product: Product; onAdd: (id: string) => void }) {
  return (
    <article>
      <h3>{product.name}</h3>
      <p>{(product.price / 100).toFixed(2)} USD</p>
      <button onClick={() => onAdd(product.id)}>Add to cart</button>
    </article>
  );
}
function ProductGrid({ products }: { products: Product[] }) {
  const [cart, setCart] = useState<string[]>([]);
  const add = (id: string) => setCart((prev) => [...prev, id]); // never mutate
  return (
    <section>
      <p>{cart.length} items in cart</p>
      {products.map((p) => <ProductCard key={p.id} product={p} onAdd={add} />)}
    </section>
  );
}

02Hook-եր. useState, useEffect, useMemo, useRef

Hook-երը ֆունկցիաներ են, որոնք կոմպոնենտներին տալիս են հիշողություն և side effect-ներ։ Կանչիր դրանք կոմպոնենտի վերին մակարդակում, երբեք պայմանների կամ ցիկլերի ներսում, որովհետև React-ը դրանց հետևում է կանչերի հերթականությամբ։ useState-ը պահում է արժեք և setter. օգտագործիր updater ձևը (setCount(c => c + 1)), երբ նոր արժեքը կախված է հնից։ useEffect-ը կոմպոնենտը սինխրոնացնում է React-ից դուրս ինչ-որ բանի հետ, օրինակ՝ subscription-ի, թայմերի կամ բրաուզերի API-ի. վերադարձրու cleanup ֆունկցիա և dependency array-ում թվարկիր այն բոլոր արժեքները, որոնք այն օգտագործում է։ Շատ effect-ներ ավելորդ են. եթե կարող ես արժեքը հաշվարկել render-ի ընթացքում, արա դա՝ այն state-ի մեջ սինխրոնացնելու փոխարեն։ useMemo-ն cache է անում թանկ հաշվարկը render-ների միջև, իսկ useCallback-ը՝ ֆունկցիան։ useRef-ը պահում է փոփոխվող արժեք, որը re-render չի առաջացնում, ամենից հաճախ՝ DOM node-ի հղում։ React 19.2-ը ավելացնում է useEffectEvent՝ effect-ի ներսում վերջին արժեքները կարդալու համար՝ առանց այն նորից աշխատեցնելու։

tsx
function SearchBox({ items }: { items: string[] }) {
  const [query, setQuery] = useState("");
  const inputRef = useRef<HTMLInputElement>(null);
  // Derived during render: no extra state, no effect
  const results = useMemo(
    () => items.filter((i) => i.toLowerCase().includes(query.toLowerCase())),
    [items, query]
  );
  // Side effect with cleanup: keyboard shortcut "/" focuses the input
  useEffect(() => {
    const onKey = (e: KeyboardEvent) => {
      if (e.key === "/") { e.preventDefault(); inputRef.current?.focus(); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, []);
 
  return (<><input ref={inputRef} value={query} onChange={(e) => setQuery(e.target.value)} />
    <ul>{results.map((r) => <li key={r}>{r}</li>)}</ul></>);
}

03Rendering և reconciliation

React-ում rendering նշանակում է կանչել քո կոմպոնենտ ֆունկցիաները՝ UI-ի նկարագրությունը ստանալու համար։ Դա չի նշանակում DOM-ին դիպչել։ Հետո React-ը համեմատում է նոր նկարագրությունը նախորդի հետ (reconciliation) և commit փուլում իրական DOM-ի վրա կիրառում է միայն տարբերությունները։ Կոմպոնենտը re-render է լինում, երբ փոխվում է նրա state-ը, երբ re-render է լինում նրա ծնողը, կամ երբ փոխվում է նրա կարդացած context-ը։ Re-render-ները սովորաբար էժան են. դանդաղները գալիս են թանկ հաշվարկներից կամ շատ մեծ ծառերից։ Key-երը React-ին ասում են, թե ցուցակի որ տարրը որն է. օգտագործիր կայուն id, ոչ թե array-ի index, այլապես React-ը կարող է վերաօգտագործել սխալ կոմպոնենտը և խառնել դրա state-ը։ Եթե որևէ դիրքում տարրի տիպը փոխվում է, React-ը դեն է նետում հին ենթածառը և դրա state-ը։ Rendering-ը պետք է մաքուր լինի. ոչ մի side effect, ոչ մի props-ի կամ state-ի mutation։ React Compiler-ը (stable 2025-ի վերջից) կարող է կոմպոնենտները memoise անել ավտոմատ՝ նվազեցնելով ձեռքով useMemo-ի և memo-ի կարիքը։

04Որտեղ պետք է ապրի state-ը

State-ի բագերի մեծ մասը գալիս է state-ը սխալ տեղում դնելուց։ Մտածիր state-ի տեսակներով։ Լոկալ UI state-ը (բաց dropdown, ձևի դաշտ) պատկանում է կոմպոնենտի ներսի useState-ին։ Ենթածառի կողմից կիսվող state-ը կամ հազվադեպ փոխվող արժեքները, ինչպես theme-ը, locale-ը կամ ընթացիկ օգտատերը, հարմար են Context-ի համար, բայց խուսափիր հաճախ փոխվող արժեքները մեկ մեծ context-ում դնելուց, որովհետև յուրաքանչյուր consumer re-render է լինում։ Գլոբալ client state-ը, ինչպես cart-ը, ֆիլտրերը կամ բազմաքայլ wizard-ը, ավելի պարզ է փոքր store-ում, ինչպես Zustand-ը, որտեղ կոմպոնենտները subscribe են անում միայն իրենց օգտագործած հատվածին։ Server state-ը, այսինքն քո API-ում ապրող տվյալները, տարբեր է. դրան պետք է caching, loading և error վիճակներ, refetch և invalidation։ Client կոմպոնենտներում դրա համար օգտագործիր TanStack Query կամ SWR, կամ fetch արա Server Component-ներում։ URL state-ը (որոնում, էջ, ֆիլտրեր) պատկանում է URL-ին, որպեսզի այն հնարավոր լինի կիսել և bookmark անել։

tsx
// Zustand: client state, components subscribe to slices
import { create } from "zustand";
type CartState = { items: string[]; add: (id: string) => void };
export const useCart = create<CartState>((set) => ({
  items: [],
  add: (id) => set((s) => ({ items: [...s.items, id] })),
}));
 
// TanStack Query: server state with caching and refetching
import { useQuery } from "@tanstack/react-query";
function Orders() {
  const count = useCart((s) => s.items.length); // re-renders only on count change
  const { data, isPending, error } = useQuery({
    queryKey: ["orders"],
    queryFn: () => fetch("/api/orders").then((r) => r.json()),
  });
  if (isPending) return <p>Loading...</p>;
  if (error) return <p>Could not load orders.</p>;
  return <p>{data.length} orders, {count} items in cart</p>;
}

05React 19. Action-ներ, useActionState և use

React 19-ը (թողարկված 2024-ի դեկտեմբերին, որին հաջորդեցին 19.1-ը և 19.2-ը) ասինխրոն UI-ը շատ ավելի պարզ դարձրեց։ Action-ը async ֆունկցիա է, որն օգտագործվում է transition-ում, ամենից տեսանելի՝ որպես ձևի action prop. React-ը հետևում է pending վիճակին, մշակում է սխալները և հաջողությունից հետո reset է անում ձևը։ useActionState-ը փաթաթում է action-ը և տալիս է դրա վերջին արդյունքը և isPending դրոշակ, ինչը իդեալական է ձևի վալիդացիայի հաղորդագրությունների համար։ useFormStatus-ը թույլ է տալիս ձևի ներսի submit կոճակին իմանալ, որ այն pending է, իսկ useOptimistic-ը սպասվող արդյունքը ցույց է տալիս անմիջապես, քանի դեռ հարցումն ընթանում է։ Նոր use API-ն render-ի ընթացքում կարդում է promise կամ context. promise-ի դեպքում կոմպոնենտը suspend է լինում մինչև այն resolve լինի, և ամենամոտ Suspense boundary-ն ցույց է տալիս fallback։ Այլ հարմարավետ փոփոխություններ. ref-ն այժմ սովորական prop է (առանց forwardRef-ի), և <title> ու <meta> tag-երը կարելի է render անել ցանկացած կոմպոնենտից։ React 19.2-ը ավելացրեց <Activity>՝ UI-ի մասերը թաքցնելու համար՝ պահպանելով դրանց state-ը։

tsx
"use client";
import { useActionState } from "react";
type State = { error?: string; ok?: boolean };
async function subscribe(_prev: State, formData: FormData): Promise<State> {
  const email = String(formData.get("email") ?? "");
  if (!email.includes("@")) return { error: "Please enter a valid email." };
  const res = await fetch("/api/subscribe", { method: "POST", body: formData });
  return res.ok ? { ok: true } : { error: "Something went wrong." };
}
export function SubscribeForm() {
  const [state, action, isPending] = useActionState(subscribe, {});
  return (
    <form action={action}>
      <label htmlFor="email">Email</label> <input id="email" name="email" type="email" required />
      <button disabled={isPending}>{isPending ? "Sending..." : "Subscribe"}</button>
      {state.error && <p role="alert">{state.error}</p>}
      {state.ok && <p role="status">Thanks for subscribing.</p>}
    </form>
  );
}

06Server Component-ներ

React Server Component-ները (RSC) կոմպոնենտներ են, որոնք աշխատում են միայն սերվերում՝ կամ build-ի ժամանակ, կամ յուրաքանչյուր հարցման համար։ Դրանք կարող են լինել async, անմիջապես կարդալ տվյալների բազայից կամ API-ից և ապահով օգտագործել գաղտնիքներ, իսկ դրանց կոդը երբեք չի ուղարկվում բրաուզերին։ Client-ին stream է արվում միայն render արված արդյունքը։ Client Component-ները, որոնք նշվում են ֆայլի վերևում "use client"-ով, ծանոթ տեսակն են. դրանք JavaScript են ուղարկում, hydrate են լինում բրաուզերում և կարող են օգտագործել state, effect-ներ և event handler-ներ։ Մտավոր մոդելը ծառ է, որտեղ արտաքին մասերը render են լինում սերվերում, իսկ ինտերակտիվ «կղզիները» client կոմպոնենտներ են։ Server կոմպոնենտները կարող են serialisable props փոխանցել client կոմպոնենտներին և նույնիսկ render անել դրանք որպես children։ Server Function-ները, որոնք նշվում են "use server"-ով, թույլ են տալիս client կոդին կանչել server-side կոդը ֆունկցիայի պես, սովորաբար ձևերի submit-ի համար։ RSC-ին պետք է framework, որն աջակցում է այն, ամենից հաճախ՝ Next.js App Router-ը։

tsx
// app/products/page.tsx  (Server Component: no "use client")
import { AddToCartButton } from "./add-to-cart-button";
 
export default async function ProductsPage() {
  const res = await fetch("https://api.example.com/products");
  const products: { id: string; name: string }[] = await res.json();
  return (
    <ul>
      {products.map((p) => (
        <li key={p.id}>
          {p.name} <AddToCartButton id={p.id} />
        </li>
      ))}
    </ul>
  );
}
 
// add-to-cart-button.tsx starts with "use client" and uses useState/onClick

Օգտագործեք, երբ

  • Ինտերակտիվ ինտերֆեյսներ՝ շատ state-ով. dashboard-ներ, ադմին պանելներ, խանութներ, էդիտորներ։
  • Թիմեր, որոնք ուզում են մեկ կոմպոնենտային մոդել վեբում (React, Next.js), desktop-ում (Electron) և մոբայլում (React Native)։
  • Design system-ներ և վերաօգտագործվող UI փաթեթներ, որոնք կիսվում են մի քանի պրոդուկտների միջև։
  • Կոնտենտային կայքեր, որոնց պետք է որոշ ինտերակտիվություն, երբ զուգակցվում են server-rendering framework-ի հետ, ինչպես Next.js-ը։

Մի՛ օգտագործեք, երբ

  • Հիմնականում ստատիկ էջ կամ պարզ brochure կայք՝ մեկ-երկու ինտերակցիայով, որտեղ մաքուր HTML-ը, CSS-ը և մի քիչ JavaScript-ը ավելի թեթև են։
  • Միայն client-side React այն էջերի համար, որոնք պետք է դասակարգվեն որոնողական համակարգերում և արագ բեռնվեն դանդաղ հեռախոսներում. փոխարենը օգտագործիր server rendering (Next.js)։

Այլընտրանքներ

  • Vue (with Nuxt)

    Template-ի վրա հիմնված կոմպոնենտներ՝ ներկառուցված reactivity-ով. մեղմ ուսուցման կոր և ուժեղ պաշտոնական էկոհամակարգ։

  • Svelte (with SvelteKit)

    Կոմպիլյատոր, որն արտադրում է շատ փոքր bundle-ներ և state-ի համար ավելի քիչ boilerplate է պահանջում։

  • Angular

    Ամբողջական, «կարծիք ունեցող» framework՝ dependency injection-ով և signal-ներով, տարածված մեծ ընկերություններում։

  • Solid, Preact

    React-ի նման API-ներ՝ fine-grained reactivity-ով կամ շատ փոքր չափով, արտադրողականության համար կրիտիկական widget-ների համար։

Լավագույն պրակտիկաներ

  1. Կոմպոնենտները պահիր փոքր և կենտրոնացված. state-ը բարձրացրու միայն այնքան, որքան անհրաժեշտ է։
  2. Արժեքները հաշվարկիր render-ի ընթացքում՝ effect-ներով state-ի մեջ սինխրոնացնելու փոխարեն։
  3. Որպես key օգտագործիր կայուն id-ներ, երբեք array-ի index՝ փոփոխվող ցուցակների համար։
  4. Server state-ը (TanStack Query, Server Component-ներ) առանձնացրու client state-ից (useState, Zustand)։
  5. Այն framework-ներում, որոնք աջակցում են, լռելյայն օգտագործիր Server Component-ներ. "use client" ավելացրու միայն այնտեղ, որտեղ ինտերակտիվություն է պետք։
  6. Օպտիմիզացնելուց առաջ չափիր React DevTools Profiler-ով. ամենուր memo ավելացնելուց առաջ դիտարկիր React Compiler-ը։
  7. Երբեք մի պահիր զգայուն տվյալներ, օրինակ՝ քարտի raw տվյալներ, persist արվող client store-երում։

Տարածված սխալներ

  • useEffect-ով props-ը state-ի մեջ պատճենելը կամ derived արժեքներ հաշվարկելը, ինչը առաջացնում է ավելորդ render-ներ և հնացած տվյալներ։
  • useEffect-ում բացակայող կամ սխալ dependency-ներ, որոնք հանգեցնում են հնացած արժեքների կամ անվերջ ցիկլերի։
  • State-ի օբյեկտները կամ array-ները անմիջապես mutate անելը, ինչի պատճառով React-ը չի տեսնում փոփոխությունը։
  • Արագ փոխվող արժեքները մեծ Context-ի մեջ դնելը, որը հավելվածի կեսն է օգտագործում։
  • Effect-ներում տվյալներ fetch անելը՝ առանց loading-ի, սխալների, race condition-ների և caching-ի մշակման։

Խորը վերլուծություն

07Fiber, lane-եր և concurrent rendering

Ներսում React-ը կոմպոնենտների ծառը ներկայացնում է fiber node-երի կապակցված կառուցվածքով՝ մեկ node յուրաքանչյուր կոմպոնենտի կամ էլեմենտի համար, որը պահում է իր props-ը, state-ը, hook-երի ցուցակը և սպասվող աշխատանքը։ Քանի որ rendering-ը բաժանված է այս fiber-ների վրա աշխատանքի փոքր միավորների, React-ը կարող է դադարեցնել render-ը, մշակել ավելի հրատապ բան և շարունակել, կամ դեն նետել չավարտված աշխատանքը։ Յուրաքանչյուր թարմացման տրվում է առաջնահերթության lane. ստեղնի սեղմումը կամ click-ը հրատապ է, իսկ startTransition-ով փաթաթված թարմացումը transition է, որը կարող է ընդհատվել։ Սա concurrent հնարավորությունների հիմքն է։ useTransition-ը state-ի թարմացումը նշում է որպես ոչ հրատապ և քեզ isPending flag է տալիս. React 19-ում callback-ը կարող է async լինել։ useDeferredValue-ն քեզ տալիս է արժեքի «հետ մնացող» պատճենը, այնպես որ ծանր ցուցակը render է լինում հին արժեքով, իսկ input-ը մնում է արձագանքող։ Commit փուլը, որտեղ React-ը փոփոխությունները կիրառում է DOM-ի վրա, երբեք չի ընդհատվում, այնպես որ օգտատերերը երբեք կիսով չափ թարմացված էկրան չեն տեսնում։ Հենց սրա համար էլ render ֆունկցիաները պետք է pure լինեն. React-ը կարող է դրանք կանչել մեկից ավելի անգամ կամ ընդհանրապես commit չանել դրանց արդյունքը։

tsx
import { useDeferredValue, useState, useTransition, memo } from "react";
 
function ProductSearch({ products }: { products: { id: string; name: string }[] }) {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query); // lags behind while busy
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState<"all" | "sale">("all");
 
  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button onClick={() => startTransition(() => setTab("sale"))}>Sale</button>
      {isPending && <span>Loading...</span>}
      <Results products={products} query={deferredQuery} tab={tab} />
    </>
  );
}
const Results = memo(function Results(props: { products: { id: string; name: string }[]; query: string; tab: string }) {
  return <ul>{props.products.filter((p) => p.name.includes(props.query)).map((p) => <li key={p.id}>{p.name}</li>)}</ul>;
});

08Hydration, mismatch-եր և Suspense boundary-ներ

Երբ էջը server-rendered է, բրաուզերը նախ ստանում է պարզ HTML։ Hydration-ը React-ի կողմից event handler-ների և state-ի կցումն է այդ առկա HTML-ին՝ client-ի վրա նույն ծառը render անելով և այն node առ node համեմատելով։ Եթե client-ի render-ը տարբեր output է տալիս, օրինակ՝ որովհետև օգտագործել է Date.now(), Math.random(), պատուհանի չափը, օգտատիրոջ locale-ը կամ localStorage, React-ը հաղորդում է hydration mismatch և կարող է այդ մասը զրոյից re-render անել՝ կորցնելով server rendering-ի օգուտը։ Ուղղիր՝ երկու կողմերում նույնական output ստանալով. արժեքը փոխանցիր սերվերից կամ միայն client-ի մասը render արա mount-ից հետո, իսկ suppressHydrationWarning-ն օգտագործիր միայն անխուսափելի մեկ տեքստային տարբերությունների համար, օրինակ՝ timestamp-ի։ Suspense boundary-ները որոշում են, թե որտեղ են հայտնվում loading վիճակները, իսկ streaming SSR-ի դեպքում դրանք նաև hydration-ի միավորներ են. յուրաքանչյուր boundary կարող է իր HTML-ը stream անել և hydrate լինել անկախ, և React-ը առաջնահերթություն է տալիս այն մասի hydration-ին, որի հետ օգտատերը փոխազդում է։ Boundary-ները տեղադրիր անկախ, դանդաղ հատվածների շուրջ, ոչ թե յուրաքանչյուր կոմպոնենտի, այլապես էջը կթարթի spinner-ներով։

tsx
import { Suspense, useSyncExternalStore } from "react";
// Same output on server and first client render, then real value
function useIsClient() {
  return useSyncExternalStore(
    () => () => {},
    () => true,   // client snapshot
    () => false,  // server snapshot
  );
}
function LocalTime({ iso }: { iso: string }) {
  const isClient = useIsClient();
  return <time dateTime={iso}>{isClient ? new Date(iso).toLocaleString() : iso.slice(0, 10)}</time>;
}
function Page() {
  return <main><ProductDetails /><Suspense fallback={<p>Loading reviews...</p>}><Reviews /></Suspense></main>;
}

09React Compiler-ը և կանոնները, որոնց վրա այն հենվում է

React Compiler-ը (1.0՝ 2025-ի հոկտեմբերից) build-time գործիք է, որը վերլուծում է քո կոմպոնենտներն ու hook-երը և ավտոմատ ավելացնում memoisation՝ ավելի մանր մակարդակով, քան մարդկանց մեծ մասը ձեռքով կգրեր. առանձին JSX էլեմենտներ և արժեքներ, ոչ միայն ամբողջական կոմպոնենտներ։ Այն կարող է դա ապահով անել միայն այն դեպքում, եթե քո կոդը հետևում է Rules of React-ին. կոմպոնենտներն ու hook-երը pure են render-ի ընթացքում, props-ը և state-ը դիտարկվում են որպես immutable, hook-երը կանչվում են անպայմանորեն վերին մակարդակում, և hook-երին փոխանցված արժեքները հետո չեն փոփոխվում։ Այս կանոնները խախտող կոդը բաց է թողնվում, ոչ թե սխալ կոմպիլյացվում, այնպես որ հավելվածը դեռ աշխատում է, բայց այդ կոմպոնենտները ոչինչ չեն շահում։ eslint-plugin-react-hooks-ի recommended կանոնները հաղորդում են նույն խախտումները, ուստի միացրու դրանք կոմպիլյատորը միացնելուց առաջ։ Ներդրումը սովորաբար աստիճանական է. միացրու այն մեկ պանակի համար, չափիր Profiler-ով, հետո ընդլայնիր։ Միացնելուց հետո ձեռքով գրված useMemo, useCallback և memo կանչերի մեծ մասն ավելորդ է դառնում, թեև առկաները դեռ հարգվում են։

tsx
// Breaks the rules: mutating during render, so the compiler skips this component
function CartTotal({ items }: { items: { price: number; qty: number }[] }) {
  items.sort((a, b) => b.price - a.price); // mutates a prop!
  return <p>{items.reduce((sum, i) => sum + i.price * i.qty, 0)}</p>;
}
 
// Follows the rules: derive a new array, no manual memo needed with the compiler
function CartTotalFixed({ items }: { items: { price: number; qty: number }[] }) {
  const sorted = items.toSorted((a, b) => b.price - a.price);
  const total = sorted.reduce((sum, i) => sum + i.price * i.qty, 0);
  return <p>{total}</p>;
}
 
// next.config.ts:  { reactCompiler: true }
// Vite: babel-plugin-react-compiler in the React plugin's babel options

10Rendering-ի արագությունը մասշտաբով՝ ցուցակներ, context և code splitting

React-ի արագության խնդիրների մեծ մասը մի քանի pattern-ի մեջ է տեղավորվում։ Շատ երկար ցուցակներ. 5,000 տողի render-ը ստեղծում է 5,000 DOM node՝ անկախ նրանից, թե որքան արագ է React-ը, ուստի virtualise արա TanStack Virtual-ի նման գրադարանով, որը render է անում միայն տեսանելի տողերը, կամ էջավորիր։ Context fan-out. հաճախ փոխվող օբյեկտ պահող մեկ context-ը re-render է անում բոլոր consumer-ներին. բաժանիր այն առանձին context-ների state-ի և action-ների համար կամ արագ փոխվող տվյալները տեղափոխիր selector-ներով store։ Չափից բարձր դրված state. էջի վերևում դաշտում տպելը re-render է անում ներքևի ամեն ինչ. state-ը տեղափոխիր ներքև՝ այն կոմպոնենտի մեջ, որին այն պատկանում է, կամ ծանր ենթածառերը փոխանցիր որպես children, որպեսզի դրանք re-render չլինեն։ Մեծ bundle-ներ. հազվադեպ օգտագործվող էկրանները բաժանիր lazy()-ով և Suspense-ով, իսկ chart-երի կամ էդիտորների նման ծանր գրադարանները import արա միայն այնտեղ, որտեղ պետք են։ Միշտ հաստատիր React DevTools Profiler-ով՝ միացնելով «Highlight updates when components render»-ը, և չափիր throttle արված միջին կարգի հեռախոսի վրա, ոչ թե ծրագրավորողի laptop-ի։

tsx
import { lazy, Suspense, useState } from "react";
 
// Code-split a heavy screen: loaded only when first rendered
const ReportsChart = lazy(() => import("./reports-chart"));
 
export function Dashboard() {
  const [showReports, setShowReports] = useState(false);
  return (
    <section>
      <button onClick={() => setShowReports(true)}>Show reports</button>
      {showReports && (
        <Suspense fallback={<p>Loading chart...</p>}>
          <ReportsChart />
        </Suspense>
      )}
    </section>
  );
}

Իրական իրավիճակներ

01

Ֆիլտրի դաշտում տպելը սառեցնում է ապրանքների ցուցակը

Խնդիր

Admin էջը ցույց է տալիս 3,000 ապրանք՝ վերևում որոնման դաշտով։ Միջին կարգի laptop-ի վրա յուրաքանչյուր ստեղնի սեղմում հայտնվելու համար 300 ms է պահանջում, և աշխատակիցները բողոքում են, որ input-ը հետ է մնում տպելուց։

Ինչպես լուծել

Նախ profile արա React DevTools Profiler-ով՝ հաստատելու, որ ծախսը ցուցակն է։ Հետո առանձնացրու հրատապ և ոչ հրատապ աշխատանքը. input-ը պահիր ուղղակիորեն կապված state-ին, իսկ ցուցակին փոխանցիր useDeferredValue(query)՝ memo-ով փաթաթված կամ React Compiler-ով կոմպիլյացված, այնպես որ տպելը մնում է ակնթարթային, իսկ ցուցակը հետո է հասնում։ Ցուցակը virtualise արա TanStack Virtual-ով, որպեսզի DOM-ում լինի ընդամենը մոտ 30 տող։ Փոքրատառ որոնման դաշտը նախապես հաշվարկիր մեկ անգամ, ոչ թե ամեն ստեղնի սեղմման ժամանակ։ Եթե տվյալները շարունակեն աճել, ֆիլտրումը տեղափոխիր սերվեր՝ debounce արված request-ով, և չեղարկիր հնացածները։ Նորից չափիր. input-ը պետք է արձագանքի մեկ frame-ի ընթացքում։

02

Hydration սխալներ՝ օգտատիրոջ արժույթով գներ ավելացնելուց հետո

Խնդիր

Գներն ու ամսաթվերը բրաուզերի locale-ով ֆորմատավորելուց հետո console-ը լցվում է hydration mismatch նախազգուշացումներով, և որոշ ապրանքների card-եր բեռնվելիս թարթում և re-render են լինում։

Ինչպես լուծել

Սերվերը render է անում իր locale-ով և ժամային գոտով, բրաուզերը՝ օգտատիրոջ, ուստի HTML-ը տարբերվում է։ Locale-ը և արժույթը որոշիր սերվերում՝ URL-ից, cookie-ից կամ Accept-Language header-ից, և փոխանցիր ներքև, որպեսզի երկու կողմերն էլ նույնական ֆորմատավորեն Intl.NumberFormat-ով։ Այն արժեքների համար, որոնք իսկապես կախված են սարքից, օրինակ՝ տեղական ժամը, սերվերում render արա կայուն placeholder, իսկ ճշգրիտ արժեքը՝ mount-ից հետո՝ useSyncExternalStore-ով client ստուգման կամ effect-ի միջոցով։ suppressHydrationWarning-ն օգտագործիր միայն մեկ տեքստային node-ի վրա՝ անխուսափելի timestamp-ի համար։ Ավելացրու թեստ, որը render է անում սերվերում և hydrate է անում jsdom-ում՝ ձախողվելով mismatch նախազգուշացումների դեպքում։

03

Ընդհանուր React package-ը բագեր է առաջացնում տասը client կայքերում

Խնդիր

Ներքին UI package-ն օգտագործվում է բազմաթիվ կայքերի կողմից։ Minor release-ը փոխեց կոմպոնենտի լռելյայն վարքագիծը և կոտրեց checkout-ը դրանցից մի քանիսում, և ոչ ոք չնկատեց, մինչև հաճախորդները չբողոքեցին։

Ինչպես լուծել

Package-ին վերաբերվիր որպես public գրադարանի։ Խստորեն հետևիր semantic versioning-ին. վարքագծի փոփոխությունները major են՝ changelog-ով և միգրացիայի նշումներով։ React-ը պահիր որպես peer dependency, որպեսզի կայքերը երբեք երկու պատճեն չբեռնեն։ Յուրաքանչյուր export արված կոմպոնենտի համար ավելացրու visual regression և interaction թեստեր, գումարած contract test, որը կոմպոնենտները render է անում յուրաքանչյուր սպառող կայքի տիպիկ props-ով։ Հրապարակիր pre-release տարբերակներ և թեստավորիր դրանք երկու-երեք ներկայացուցչական կայքում stable release-ից առաջ։ Վարքագծի փոփոխությունների համար օգտագործիր feature flag-եր կամ նոր props՝ լռելյայն արժեքները փոխելու փոխարեն։ Վերջապես, գրանցիր, թե որ կայքերն են որ տարբերակն օգտագործում, որպեսզի ուղղումը կարելի լինի տարածել վերահսկվող հերթականությամբ։

Արագ ուղեցույց

Թարմացումը նշել որպես ոչ հրատապ
const [isPending, startTransition] = useTransition();
Input-ը արագ պահել, ծանր ցուցակին թույլ տալ հետ մնալ
const deferred = useDeferredValue(query);
Կոմպոնենտի code splitting
const Chart = lazy(() => import("./chart"));
Վերակայել կոմպոնենտի state-ը, երբ id-ն փոխվում է
<Profile key={userId} userId={userId} />
Ապահով բաժանորդագրվել բրաուզերի API-ին
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
Կարդալ վերջին արժեքները effect-ում՝ առանց այն վերագործարկելու
const onTick = useEffectEvent(() => log(count));
Optimistic UI mutation-ի ընթացքում
const [optimistic, addOptimistic] = useOptimistic(items, reducer);
Միացնել React Compiler-ը Next.js-ում
// next.config.ts { reactCompiler: true }
Lint անել Rules of React-ը
npm i -D eslint-plugin-react-hooks // use its recommended config
Գտնել դանդաղ render-ները
React DevTools > Profiler > record > sort by render duration

Իմ իրական նախագծերում

  • Ներքին React փաթեթների էկոհամակարգի հիմնական contributor-ներից եմ (մոտ 1000 իմ commit-ներ)՝ config-driven ադմին համակարգով, մոտ 20 կոմպոնենտից բաղկացած design system-ով և account, cart ու form փաթեթներով, որոնք օգտագործվում են մոտ 10 հաճախորդների կայքերում։
  • Այդ էկոհամակարգում memoise արեցի provider-ները, loading counter-ը դարձրի ապահով server rendering-ի համար և քարտի raw տվյալները հեռու պահեցի persist արվող cart store-ից՝ ստուգելով, որ server-rendered արդյունքը մնաց նույնը։
  • Multi-tenant POS SaaS-ն օգտագործում է React 19 Vite-ով, TanStack Query՝ server state-ի համար և Zustand՝ client state-ի համար։
  • Իմ հինգ React Native հավելվածները կիսում են մեկ template. սուրճ պատվիրելու հավելվածում էկրանները կարդում են միայն store-երից, ինչը տվյալների տրամաբանությունը հեռու է պահում UI-ից։

Հարցեր և պատասխաններ

Q1Ո՞րն է state-ի և props-ի տարբերությունը։

Props-ը մուտքեր են, որոնք ծնողից փոխանցվում են երեխա կոմպոնենտին։ Երեխան կարդում է դրանք, բայց չպետք է փոխի, ինչպես ֆունկցիայի արգումենտները։ State-ը տվյալներ են, որոնք կոմպոնենտը տիրապետում է և կարող է ժամանակի ընթացքում թարմացնել, օրինակ՝ ձևի մուտքագրումը, բաց կամ փակ մենյուն կամ ընտրված tab-ը։ Երբ state-ը փոխվում է, React-ը re-render է անում կոմպոնենտը. երբ ծնողը re-render է լինում նոր props-ով, երեխան նույնպես re-render է լինում։ Լավ կանոն. եթե արժեքը կարելի է հաշվարկել props-ից կամ այլ state-ից, այն ընդհանրապես չպետք է state լինի։ Եթե երկու sibling-ի պետք է նույն state-ը, տեղափոխիր այն նրանց ընդհանուր ծնողի մոտ և փոխանցիր ներքև որպես props։

Q2Ինչո՞ւ են key-երը կարևոր ցուցակներ render անելիս։

Key-երը թույլ են տալիս React-ին reconciliation-ի ժամանակ համապատասխանեցնել ցուցակի տարրերը render-ների միջև։ Կայուն key-երով, սովորաբար տվյալների բազայի id-ներով, React-ը գիտի, որ 42 id-ով տարրը երրորդ դիրքից տեղափոխվել է առաջին և կարող է պահպանել դրա DOM node-ը և ներքին state-ը։ Array-ի index-ները որպես key օգտագործելիս տարր ավելացնելը կամ հեռացնելը տեղաշարժում է բոլոր index-ները, և React-ը կարող է վերաօգտագործել սխալ կոմպոնենտը. input-ը պահում է այլ տողի տեքստ, կամ անիմացիաներն ու focus-ը ցատկոտում են։ Key-երը պետք է եզակի լինեն sibling-ների միջև և կայուն՝ render-ների միջև. երբեք դրանք պատահականորեն մի գեներացրու render-ի ընթացքում, որովհետև դա ստիպում է React-ին ամեն անգամ վերստեղծել յուրաքանչյուր տարր։

Q3Ե՞րբ պետք է օգտագործել useEffect, և ե՞րբ՝ ոչ։

useEffect-ը React-ից դուրս համակարգերի հետ սինխրոնացման համար է՝ subscription-ներ, թայմերներ, WebSocket-ներ, բրաուզերի API-ներ, երրորդ կողմի widget-ներ կամ անալիտիկա։ Այն աշխատում է commit-ից հետո, իսկ դրա cleanup-ը՝ հաջորդ effect-ից առաջ և unmount-ի ժամանակ։ Սովորաբար այն պետք չէ derived տվյալների համար (հաշվարկիր render-ի ընթացքում, useMemo-ով, եթե թանկ է), օգտատիրոջ event-ների մշակման համար (տրամաբանությունը դիր event handler-ում) կամ prop-ի փոփոխության ժամանակ state-ը reset անելու համար (օգտագործիր key)։ Effect-ների ներսում տվյալների fetch-ը աշխատում է, բայց պահանջում է race condition-ների մշակում և caching, ուստի նախընտրում եմ TanStack Query, SWR կամ Server Component-ներ։ Ավելի քիչ effect-ներ՝ ավելի քիչ բագեր։

Q4Ինչպե՞ս ես ընտրում Context-ի, Zustand-ի և TanStack Query-ի միջև։

Ըստ state-ի տեսակի։ TanStack Query-ն (կամ SWR-ը) server state-ի համար է. API-ին պատկանող տվյալներ, որոնց պետք է caching, background refetch, deduplication և invalidation՝ mutation-ներից հետո։ Zustand-ը հաճախ փոխվող գլոբալ client state-ի համար է, ինչպես cart-ը, ֆիլտրերը կամ բազմաքայլ հոսքը. կոմպոնենտները subscribe են անում հատվածներին, ուստի re-render է լինում միայն այն, ինչ փոխվել է։ Context-ը լավագույնն է dependency injection-ի և հազվադեպ փոխվող արժեքների համար, ինչպես theme-ը, locale-ը կամ ընթացիկ օգտատերը. այն յուրաքանչյուր փոփոխության ժամանակ re-render է անում բոլոր consumer-ներին, ուստի վատ store է արագ փոխվող տվյալների համար։ Լոկալ useState-ը դեռ լռելյայնն է այն ամենի համար, ինչ պետք է միայն մեկ կոմպոնենտի։

Q5Ի՞նչ են React Server Component-ները, և ինչո՞վ են տարբերվում SSR-ից։

Server-side rendering-ը (SSR) client կոմպոնենտները սերվերում render է անում HTML-ի՝ արագ առաջին paint-ի համար, բայց դրանց ամբողջ JavaScript-ը դեռ ուղարկվում և hydrate է լինում բրաուզերում։ Server Component-ները կոմպոնենտների այլ տեսակ են. դրանք աշխատում են միայն սերվերում, կարող են լինել async և անմիջապես դիմել տվյալների բազաներին կամ գաղտնիքներին, իսկ դրանց կոդը երբեք չի հասնում client-ին։ Ներքև stream է արվում միայն դրանց render արված արդյունքը, իսկ ինտերակտիվ մասերը Client Component-ներ են՝ նշված "use client"-ով։ Երկուսն աշխատում են միասին. Next.js էջը render է անում Server Component-ները, Client Component-ները server-render է անում HTML-ի, հետո hydrate է անում միայն client մասերը։ Արդյունքը ավելի քիչ JavaScript և ավելի պարզ data fetching է։

Q6Ի՞նչ փոխվեց React 19-ում, որը կարևոր է ամենօրյա աշխատանքում։

Ամենամեծ փոփոխությունը Action-ներն են. async ֆունկցիաներ, որոնք React-ը աշխատեցնում է transition-ում, սովորաբար ձևի action prop-ի միջոցով։ useActionState-ի, useFormStatus-ի և useOptimistic-ի հետ միասին դրանք հեռացնում են ձևերում ձեռքով գրված loading, error և optimistic-update կոդի մեծ մասը։ use API-ն render-ի ընթացքում կարդում է promise-ներ և context՝ աշխատելով Suspense-ի հետ։ ref-ն այժմ սովորական prop է, ուստի forwardRef-ն այլևս պետք չէ, և կոմպոնենտները կարող են անմիջապես render անել <title> և <meta> tag-եր։ Server Component-ները և Server Function-ները stable են framework-ների համար։ React 19.2-ը ավելացրեց <Activity> և useEffectEvent, իսկ React Compiler-ը հասավ 1.0-ի՝ ավտոմատացնելով memoisation-ի մեծ մասը, որը մարդիկ նախկինում գրում էին ձեռքով։

Սենիոր մակարդակի հարցեր

S1Ինչպե՞ս կնախագծես state management-ը բազմաթիվ թիմերով մեծ React հավելվածի համար։

Նախ դասակարգում եմ state-ը, որովհետև խնդիրների մեծ մասը գալիս է տեսակները խառնելուց։ Server state-ը անցնում է մեկ տվյալային շերտով՝ TanStack Query կամ Server Components, համաձայնեցված query-key պայմանավորվածություններով և invalidation-ի կանոններով, այնպես որ caching-ը հետևողական է, և ոչ ոք API տվյալները չի պատճենում store-երում։ URL state-ը՝ ֆիլտրեր, էջավորում և tab-եր, ապրում է router-ում, որպեսզի կարելի լինի կիսվել։ Գլոբալ client state-ը փոքր է պահվում, բաժանված feature-ներին պատկանող Zustand store-երի՝ selector-ներով, և յուրաքանչյուր store ունի հստակ տեր թիմ։ Մնացած ամեն ինչ մնում է լոկալ։ Feature-ների միջև հաղորդակցությունն անցնում է բացահայտ interface-ների, event-ների կամ ընդհանուր hook-երի միջոցով, ոչ թե ուրիշ թիմի store-ի մեջ ձեռք մտցնելով։ Փոխզիջումը ավելի շատ կառուցվածքն է և feature-ների միջև որոշ կրկնություն, որն ընդունվում է՝ փոխարենը թիմերը կարողանում են փոխել իրենց հատվածը՝ առանց ուրիշներին կոտրելու։

S2Ի՞նչ խնդիրներ են լուծում Server Components-ը, և ի՞նչն են դրանք ավելի բարդ դարձնում։

Դրանք լուծում են երեք իրական խնդիր։ Bundle-ի չափ. կոդը, որը միայն տվյալներ է ֆորմատավորում, markdown render անում կամ տվյալների բազա կանչում, մնում է սերվերում, այնպես որ բրաուզերները ավելի քիչ JavaScript են ներբեռնում և parse անում։ Տվյալների ստացում. կոմպոնենտները կարող են ուղղակիորեն await անել տվյալները՝ օգտագործման վայրին մոտ, առանց client waterfall-ների կամ միայն UI-ի համար կառուցված API շերտի։ Անվտանգություն. գաղտնիքները և ներքին ծառայությունները երբեք չեն հասնում client-ին։ Ավելի բարդ է դառնում մտավոր մոդելը. ծրագրավորողները պետք է իմանան, թե ամեն ֆայլ որ կողմում է աշխատում, ինչը կարող է անցնել սահմանը (միայն serialisable props) և ինչպես են աշխատում caching-ը և revalidation-ը իրենց framework-ում։ Debugging-ը ընդգրկում է երկու միջավայր, իսկ context կամ effect-ներ օգտագործող երրորդ կողմի գրադարաններին client wrapper-ներ են պետք։ Դրանք նաև քեզ կապում են այն framework-ին, որն իրականացնում է դրանք։ Login-ի հետևում բարձր ինտերակտիվ հավելվածների համար client-side SPA-ն դեռ կարող է ավելի պարզ ընտրություն լինել։

S3Տվյալների ստացում useEffect-ով, TanStack Query, թե Server Components. ինչպե՞ս ես ընտրում։

useEffect-ում «հում» fetch-ը ամենաթույլ լռելյայն տարբերակն է. race condition-ները, չեղարկումը, caching-ը, deduplication-ը, retry-ները և refetch-ը պետք է ինքդ մշակես, և այն request waterfall-ներ է ստեղծում, ուստի օգտագործում եմ միայն պարզ, մեկուսացված դեպքերում։ TanStack Query-ն ճիշտ գործիքն է client-rendered հավելվածների և բարձր ինտերակտիվ տվյալների համար՝ dashboard-ներ, admin աղյուսակներ, անվերջ ցուցակներ, optimistic mutation-ներ և ֆոնային թարմացում, երբ tab-ը նորից focus է ստանում։ Server Components-ը հարմար է նախնական էջի render-ի համար անհրաժեշտ տվյալներին, հատկապես public, SEO-ի համար կարևոր կամ գաղտնիքներից կախված տվյալներին, որովհետև դրանք գալիս են HTML-ի հետ և client JavaScript չեն ավելացնում։ Next.js հավելվածում սովորաբար համադրում եմ դրանք. Server Components առաջին render-ի համար և Server Actions mutation-ների համար, իսկ TanStack Query-ն պահում եմ այն մասերի համար, որոնց պետք են live client-side թարմացումներ, polling կամ բարդ caching։

Բառարան

Fiber
React-ի ներքին աշխատանքի միավոր. մեկ node յուրաքանչյուր կոմպոնենտի կամ էլեմենտի համար, որը պահում է props, state և սպասվող թարմացումներ։
Reconciliation
Նոր էլեմենտների ծառի համեմատումը նախորդի հետ՝ DOM-ի նվազագույն փոփոխությունները որոշելու համար։
Commit phase
Անընդհատելի քայլ, որտեղ React-ը հաշվարկված փոփոխությունները կիրառում է DOM-ի վրա և աշխատեցնում layout effect-ները։
Transition
State-ի ոչ հրատապ թարմացում, որը React-ը կարող է ընդհատել, որպեսզի հրատապ ինտերակցիաները, օրինակ՝ տպելը, արձագանքող մնան։
Hydration
React-ի state-ի և event handler-ների կցումը սերվերում render արված HTML-ին։
Hydration mismatch
Client-ի առաջին render-ը տարբերվում է սերվերի HTML-ից՝ ստիպելով React-ին re-render անել այդ մասը։
Suspense boundary
Կոմպոնենտ, որը fallback է ցույց տալիս, մինչ իր children-ը սպասում են կոդի կամ տվյալների, և stream է լինում անկախ։
Virtualisation
Միայն էկրանին ներկայումս տեսանելի ցուցակի տողերի render՝ տվյալների բոլոր տողերի փոխարեն։
Rules of React
Purity-ի, immutability-ի և hook-երի հերթականության կանոններ, որոնք ապահով են դարձնում React-ի օպտիմալացումները և կոմպիլյատորը։
Peer dependency
Package, որը գրադարանը սպասում է, որ host հավելվածը կտրամադրի, օրինակ՝ React-ը, որպեսզի բեռնվի միայն մեկ պատճեն։

Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։

Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։