Все уроки
ФронтендСредний14 мин чтенияВ моём стеке

React

Интерфейсы из компонентов: состояние, хуки, рендеринг и серверные возможности React 19.

  • React 19
  • Hooks
  • State
  • Server Components
  • Zustand
Шаг 1/5
React-приложение — это дерево компонентов: сверху App, под ним Header и Cart, а у них свои дети.

За 30 секунд

React — JavaScript-библиотека для построения пользовательских интерфейсов из небольших переиспользуемых частей, которые называются компонентами. Вы описываете, как должен выглядеть экран при данном состоянии, а React эффективно обновляет страницу, когда это состояние меняется. React 19 добавляет Actions для форм, хук use и Server Components, которые рендерятся на сервере и отправляют в браузер меньше JavaScript.

Почему я это использую

  • Я использую React почти для каждого интерфейса, который делаю: витрины на Next.js, админки, кассовое приложение на Electron и — через React Native — мобильные приложения.
  • Мне нравится, что компонентная модель масштабируется: одни и те же паттерны работают и для одной кнопки, и для админ-системы с десятками экранов.
  • Я поддерживаю переиспользуемые React-пакеты, включая конфигурируемую админ-систему и модули аккаунта и корзины, которые используются на многих клиентских сайтах.
  • Инструменты для состояния я выбираю осознанно: сначала локальное состояние, Zustand для клиентского состояния и библиотека серверного кэша вроде TanStack Query для данных API.

Как это работает

01Компоненты, props и состояние

Компонент — это функция, которая возвращает JSX, синтаксис, похожий на HTML и описывающий UI. Компоненты собираются, как кубики: ProductCard внутри ProductGrid внутри ShopPage. Props — это входные данные, которые родитель передаёт потомку; с точки зрения потомка они только для чтения, как аргументы функции. Состояние — это данные, которыми компонент владеет и может менять, например открыто ли меню или что введено в поле. Когда состояние меняется, React заново рендерит этот компонент и его потомков с новыми значениями. Главная идея — «UI есть функция от состояния»: вы никогда не трогаете DOM руками, а меняете состояние и описываете результат. Данные текут вниз через props, изменения — вверх через колбэк-props вроде onChange. Когда двум компонентам нужно одно и то же состояние, поднимите его к ближайшему общему родителю.

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>
  );
}

02Хуки: useState, useEffect, useMemo, useRef

Хуки — это функции, которые дают компонентам память и побочные эффекты. Вызывайте их на верхнем уровне компонента, никогда внутри условий или циклов, потому что React отслеживает их по порядку вызова. useState хранит значение и сеттер; используйте функцию-апдейтер (setCount(c => c + 1)), когда новое значение зависит от старого. useEffect синхронизирует компонент с чем-то вне React — подпиской, таймером или API браузера; возвращайте функцию очистки и перечисляйте в массиве зависимостей каждое используемое значение. Многие эффекты не нужны: если значение можно вычислить во время рендера, сделайте это, а не синхронизируйте его в состояние. useMemo кэширует дорогое вычисление между рендерами, а useCallback кэширует функцию. useRef хранит изменяемое значение, которое не вызывает ре-рендер, — чаще всего ссылку на DOM-узел. React 19.2 добавляет useEffectEvent, чтобы читать актуальные значения внутри эффекта без его перезапуска.

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></>);
}

03Рендеринг и reconciliation

Рендеринг в React означает вызов ваших функций-компонентов, чтобы получить описание UI. Это не означает прикосновения к DOM. Затем React сравнивает новое описание с предыдущим (reconciliation) и применяет к настоящему DOM только различия — в фазе commit. Компонент ре-рендерится, когда меняется его состояние, когда ре-рендерится родитель или когда меняется контекст, который он читает. Ре-рендеры обычно дешёвые; медленные возникают из-за дорогих вычислений или очень больших деревьев. Ключи (keys) говорят React, какой элемент списка какой: используйте стабильный id, а не индекс массива, иначе React может переиспользовать не тот компонент и перепутать его состояние. Если тип элемента на какой-то позиции меняется, React выбрасывает старое поддерево вместе с его состоянием. Рендеринг должен быть чистым: никаких побочных эффектов, никаких мутаций props или состояния. React Compiler (стабилен с конца 2025 года) умеет мемоизировать компоненты автоматически, уменьшая потребность в ручных useMemo и memo.

04Где должно жить состояние

Большинство багов с состоянием возникает из-за того, что оно лежит не там. Думайте о видах состояния. Локальное UI-состояние (открытый dropdown, поле формы) — в useState внутри компонента. Состояние, общее для поддерева, или редко меняющиеся значения вроде темы, локали или текущего пользователя подходят для Context, но не кладите часто меняющиеся значения в один большой контекст, потому что ре-рендерится каждый потребитель. Глобальное клиентское состояние — корзина, фильтры или многошаговый мастер — проще держать в небольшом сторе вроде Zustand, где компоненты подписываются только на нужный им срез. Серверное состояние, то есть данные, живущие в вашем API, — совсем другое: ему нужны кэширование, состояния загрузки и ошибки, повторная загрузка и инвалидация. Для этого в клиентских компонентах используйте TanStack Query или SWR, либо загружайте данные в Server Components. Состояние URL (поиск, страница, фильтры) должно жить в URL, чтобы им можно было делиться и добавлять в закладки.

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: Actions, useActionState и use

React 19 (вышел в декабре 2024 года, затем последовали 19.1 и 19.2) сильно упростил асинхронный UI. Action — это асинхронная функция, используемая в transition, нагляднее всего — в prop action у формы: React отслеживает состояние ожидания, обрабатывает ошибки и сбрасывает форму после успеха. useActionState оборачивает action и даёт вам его последний результат и флаг isPending — идеально для сообщений валидации формы. useFormStatus позволяет кнопке отправки внутри формы знать, что идёт отправка, а useOptimistic сразу показывает ожидаемый результат, пока выполняется запрос. Новый API use читает промис или контекст во время рендера; с промисом компонент приостанавливается, пока тот не зарезолвится, а ближайшая граница Suspense показывает fallback. Другие удобства: ref теперь обычный prop (без forwardRef), а теги <title> и <meta> можно рендерить из любого компонента. React 19.2 добавил <Activity>, чтобы скрывать части UI, сохраняя их состояние.

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 Components

React Server Components (RSC) — это компоненты, которые выполняются только на сервере: во время сборки или на каждый запрос. Они могут быть асинхронными, читать данные напрямую из базы или API и безопасно использовать секреты, а их код никогда не отправляется в браузер. Клиенту стримится только отрендеренный результат. Client Components, помеченные "use client" в начале файла, — привычный вид компонентов: они отправляют JavaScript, гидрируются в браузере и могут использовать состояние, эффекты и обработчики событий. Ментальная модель — дерево, внешние части которого рендерятся на сервере, а интерактивные «острова» — клиентские компоненты. Серверные компоненты могут передавать клиентским сериализуемые props и даже рендерить их как children. Server Functions, помеченные "use server", позволяют клиентскому коду вызывать серверный код как функцию — обычно для отправки форм. Для RSC нужен поддерживающий их фреймворк, чаще всего 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

Когда применять

  • Интерактивные интерфейсы с большим количеством состояния: дашборды, админки, магазины, редакторы.
  • Команды, которым нужна одна компонентная модель для веба (React, Next.js), десктопа (Electron) и мобильных (React Native).
  • Дизайн-системы и переиспользуемые UI-пакеты, общие для нескольких продуктов.
  • Контентные сайты, которым нужно немного интерактивности, — в паре с фреймворком с серверным рендерингом вроде Next.js.

Когда не стоит

  • Почти статичная страница или простой сайт-визитка с одним-двумя взаимодействиями, где обычные HTML, CSS и немного JavaScript легче.
  • Чисто клиентский React для страниц, которые должны ранжироваться в поисковиках и быстро грузиться на медленных телефонах; используйте серверный рендеринг (Next.js).

Альтернативы

  • Vue (с Nuxt)

    Компоненты на шаблонах со встроенной реактивностью; плавная кривая обучения и сильная официальная экосистема.

  • Svelte (с SvelteKit)

    Компилятор, который выдаёт очень маленькие бандлы и требует меньше шаблонного кода для состояния.

  • Angular

    Полноценный фреймворк со своими правилами, внедрением зависимостей и signals, популярен в крупных компаниях.

  • Solid, Preact

    API в стиле React с точечной реактивностью или крошечным размером — для виджетов, критичных к производительности.

Лучшие практики

  1. Делайте компоненты маленькими и сфокусированными; поднимайте состояние ровно настолько, насколько нужно.
  2. Вычисляйте производные значения во время рендера, а не синхронизируйте их в состояние через эффекты.
  3. Используйте стабильные id в качестве ключей, никогда не индекс массива для списков, которые могут меняться.
  4. Разделяйте серверное состояние (TanStack Query, Server Components) и клиентское (useState, Zustand).
  5. В фреймворках с поддержкой Server Components используйте их по умолчанию; добавляйте "use client" только там, где нужна интерактивность.
  6. Перед оптимизацией измеряйте через React DevTools Profiler; прежде чем расставлять memo везде, подумайте о React Compiler.
  7. Никогда не храните чувствительные данные, например сырые данные карты, в персистентных клиентских сторах.

Частые ошибки

  • Использовать useEffect, чтобы копировать props в состояние или вычислять производные значения, получая лишние рендеры и устаревшие данные.
  • Пропущенные или неверные зависимости в useEffect, ведущие к устаревшим значениям или бесконечным циклам.
  • Мутировать объекты или массивы состояния напрямую, из-за чего React не видит изменения.
  • Класть быстро меняющиеся значения в большой Context, который потребляет пол-приложения.
  • Загружать данные в эффектах без обработки загрузки, ошибок, гонок и кэширования.

Глубже

07Fiber, lanes и конкурентный рендеринг

Внутри React представляет дерево компонентов как связанную структуру fiber-узлов — по одному на компонент или элемент, — каждый из которых хранит свои props, state, список хуков и отложенную работу. Поскольку рендеринг разбит на небольшие единицы работы над этими fiber'ами, React может приостановить рендер, обработать что-то более срочное и продолжить, либо выбросить незаконченную работу. Каждому обновлению назначается приоритетная полоса (lane): нажатие клавиши или клик срочные, а обновление, обёрнутое в startTransition, — это transition, который можно прервать. На этом строятся конкурентные возможности. useTransition помечает обновление state как несрочное и даёт флаг isPending; в React 19 колбэк может быть асинхронным. useDeferredValue даёт отстающую копию значения, так что тяжёлый список рендерится со старым значением, а input остаётся отзывчивым. Фаза commit, в которой React применяет изменения к DOM, никогда не прерывается, поэтому пользователи никогда не видят наполовину обновлённый экран. Именно поэтому функции рендера должны быть чистыми: React может вызвать их больше одного раза или вообще не закоммитить результат.

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>;
});

08Гидрация, рассинхронизации и границы Suspense

Когда страница отрендерена на сервере, браузер сначала получает обычный HTML. Гидрация — это когда React подключает обработчики событий и state к уже существующему HTML, рендеря то же дерево на клиенте и сопоставляя его узел за узлом. Если клиентский рендер даёт другой результат, например потому что использовал Date.now(), Math.random(), размер окна, локаль пользователя или localStorage, React сообщает о hydration mismatch и может перерендерить эту часть с нуля, теряя выгоду от серверного рендеринга. Исправляйте это, добиваясь одинакового результата на обеих сторонах: передайте значение с сервера или рендерьте клиентскую часть после монтирования, а suppressHydrationWarning используйте только для неизбежных различий в одном тексте, например в timestamp. Границы Suspense определяют, где появляются состояния загрузки, а при потоковом SSR они ещё и задают единицы гидрации: каждая граница может стримить свой HTML и гидрироваться независимо, а React в первую очередь гидрирует ту часть, с которой взаимодействует пользователь. Ставьте границы вокруг независимых медленных секций, а не вокруг каждого компонента, иначе страница будет мигать спиннерами.

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 года) — инструмент этапа сборки, который анализирует ваши компоненты и хуки и автоматически добавляет мемоизацию, причём более гранулярно, чем большинство написали бы вручную: на уровне отдельных JSX-элементов и значений, а не только целых компонентов. Безопасно делать это он может, только если код следует Rules of React: компоненты и хуки чисты во время рендера, props и state считаются неизменяемыми, хуки вызываются безусловно на верхнем уровне, а переданные в хуки значения потом не мутируются. Код, нарушающий эти правила, пропускается, а не компилируется неправильно, так что приложение продолжает работать, но такие компоненты ничего не выигрывают. Рекомендуемые правила в eslint-plugin-react-hooks сообщают о тех же нарушениях, поэтому включите их до включения компилятора. Внедрение обычно постепенное: включите его для одной директории, измерьте через 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

10Производительность рендеринга в масштабе: списки, контекст и code splitting

Большинство проблем производительности React укладываются в несколько паттернов. Очень длинные списки: рендер 5,000 строк создаёт 5,000 DOM-узлов, как бы быстр ни был React, поэтому виртуализируйте библиотекой вроде TanStack Virtual, которая рендерит только видимые строки, или делайте пагинацию. Веерные перерендеры от контекста: один контекст с часто меняющимся объектом перерендеривает всех потребителей; разделите его на отдельные контексты для state и действий или вынесите быстро меняющиеся данные в store с селекторами. State, поднятый слишком высоко: ввод в поле наверху страницы перерендеривает всё ниже; опустите state в компонент, которому он принадлежит, или передавайте тяжёлые поддеревья как children, чтобы они не перерендеривались. Большие бандлы: выносите редко используемые экраны через lazy() и Suspense и импортируйте тяжёлые библиотеки вроде графиков или редакторов только там, где они нужны. Всегда подтверждайте выводы через React DevTools Profiler, включив «Highlight updates when components render», и измеряйте на замедленном телефоне среднего класса, а не на ноутбуке разработчика.

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

Ввод в поле фильтра замораживает список товаров

Проблема

Админская страница показывает 3,000 товаров с полем поиска над ними. На ноутбуке среднего уровня каждое нажатие клавиши отображается через 300 ms, и сотрудники жалуются, что input отстаёт от набора.

Как решить

Сначала профилируйте через React DevTools Profiler, чтобы подтвердить, что дорого обходится именно список. Затем разделите срочную и несрочную работу: держите input напрямую привязанным к state, а в список передавайте useDeferredValue(query), обернув его в memo или положившись на React Compiler, — так ввод остаётся мгновенным, а список догоняет. Виртуализируйте список через TanStack Virtual, чтобы в DOM было всего около 30 строк. Предвычислите поле поиска в нижнем регистре один раз, а не на каждое нажатие. Если данных станет ещё больше, перенесите фильтрацию на сервер с debounce-запросом и отменяйте устаревшие. Измерьте снова: input должен реагировать в пределах одного кадра.

02

Ошибки гидрации после добавления цен в валюте пользователя

Проблема

После форматирования цен и дат по локали браузера консоль заполняется предупреждениями о hydration mismatch, а некоторые карточки товаров мигают и перерендериваются при загрузке.

Как решить

Сервер рендерит со своей локалью и часовым поясом, браузер — с пользовательскими, поэтому HTML различается. Определяйте локаль и валюту на сервере — по URL, cookie или заголовку Accept-Language — и передавайте их вниз, чтобы обе стороны форматировали одинаково через Intl.NumberFormat. Для значений, действительно зависящих от устройства, например локального времени, рендерьте на сервере стабильный плейсхолдер, а точное значение — после монтирования, используя клиентскую проверку через useSyncExternalStore или эффект. suppressHydrationWarning используйте только на одном текстовом узле для неизбежного timestamp. Добавьте тест, который рендерит на сервере и гидрирует в jsdom, падая при предупреждениях о рассинхронизации.

03

Общий React-пакет вызывает баги на десяти клиентских сайтах

Проблема

Внутренний UI-пакет используется многими сайтами. Минорный релиз изменил поведение компонента по умолчанию и сломал checkout на нескольких из них, и никто этого не заметил, пока не начали жаловаться клиенты.

Как решить

Относитесь к пакету как к публичной библиотеке. Строго следуйте семантическому версионированию: изменения поведения — это мажорные версии с changelog'ом и заметками о миграции. Держите React в peer dependency, чтобы сайты никогда не загружали две копии. Добавьте визуальные регрессионные тесты и тесты взаимодействия для каждого экспортируемого компонента, а также контрактный тест, который рендерит компоненты с типичными props каждого сайта-потребителя. Публикуйте пре-релизные версии и проверяйте их на двух-трёх показательных сайтах до стабильного релиза. Для изменений поведения используйте feature flags или новые 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)
Читать последние значения в эффекте без его перезапуска
const onTick = useEffectEvent(() => log(count));
Оптимистичный UI во время мутации
const [optimistic, addOptimistic] = useOptimistic(items, reducer);
Включить React Compiler в Next.js
// next.config.ts { reactCompiler: true }
Проверять Rules of React линтером
npm i -D eslint-plugin-react-hooks // use its recommended config
Найти медленные рендеры
React DevTools > Profiler > record > sort by render duration

В моих реальных проектах

  • Я один из основных контрибьюторов внутренней экосистемы React-пакетов (около 1000 моих коммитов): конфигурируемая админ-система, дизайн-система примерно из 20 компонентов и пакеты аккаунта, корзины и форм, используемые примерно на 10 клиентских сайтах.
  • В этой экосистеме я мемоизировал провайдеры, сделал счётчик загрузки безопасным для серверного рендеринга и убрал сырые данные карты из персистентного стора корзины, проверив, что отрендеренный на сервере результат остался идентичным.
  • Мультитенантный POS SaaS использует React 19 с Vite, TanStack Query для серверного состояния и Zustand для клиентского.
  • Пять моих приложений на React Native используют один шаблон; в приложении для заказа кофе экраны читают данные только из сторов, что держит логику данных вне UI.

Вопросы и ответы

Q1Чем состояние отличается от props?

Props — это входные данные, которые родитель передаёт дочернему компоненту. Потомок их читает, но не должен менять, примерно как аргументы функции. Состояние — это данные, которыми компонент владеет и может обновлять со временем: ввод в форме, открытое или закрытое меню, выбранная вкладка. Когда состояние меняется, React ре-рендерит компонент; когда родитель ре-рендерится с новыми props, потомок тоже ре-рендерится. Хорошее правило: если значение можно вычислить из props или другого состояния, оно вообще не должно быть состоянием. Если двум соседним компонентам нужно одно и то же состояние, перенесите его в общего родителя и передавайте вниз через props.

Q2Почему ключи важны при рендеринге списков?

Ключи позволяют React сопоставлять элементы списка между рендерами во время reconciliation. Со стабильными ключами, обычно id из базы данных, React знает, что элемент с id 42 переместился с третьей позиции на первую, и может сохранить его DOM-узел и внутреннее состояние. Если ключами служат индексы массива, вставка или удаление элемента сдвигает все индексы, и React может переиспользовать не тот компонент: поле ввода сохраняет текст, принадлежащий другой строке, или анимации и фокус «прыгают». Ключи должны быть уникальными среди соседей и стабильными между рендерами; никогда не генерируйте их случайно во время рендера, потому что это заставляет React каждый раз пересоздавать каждый элемент.

Q3Когда стоит использовать useEffect, а когда нет?

useEffect нужен для синхронизации с системами вне React: подписками, таймерами, WebSocket'ами, API браузера, сторонними виджетами или аналитикой. Он выполняется после commit, а его очистка — перед следующим эффектом и при размонтировании. Обычно он не нужен для производных данных (вычисляйте их во время рендера, через useMemo, если дорого), для обработки событий пользователя (кладите логику в обработчик события) или для сброса состояния при смене prop (используйте key). Загрузка данных внутри эффектов работает, но требует обработки гонок и кэширования, поэтому я предпочитаю TanStack Query, SWR или Server Components. Меньше эффектов — меньше багов.

Q4Как выбрать между Context, Zustand и TanStack Query?

По виду состояния. TanStack Query (или SWR) — для серверного состояния: данных, которыми владеет API и которым нужны кэширование, фоновая повторная загрузка, дедупликация и инвалидация после мутаций. Zustand — для глобального клиентского состояния, которое часто меняется, вроде корзины, фильтров или многошагового сценария; компоненты подписываются на срезы, поэтому ре-рендерится только то, что изменилось. Context лучше всего подходит для внедрения зависимостей и редко меняющихся значений — темы, локали или текущего пользователя; он ре-рендерит всех потребителей при каждом изменении, поэтому плохо подходит как стор для быстро меняющихся данных. Локальный useState по-прежнему выбор по умолчанию для всего, что нужно только одному компоненту.

Q5Что такое React Server Components и чем они отличаются от SSR?

Серверный рендеринг (SSR) рендерит клиентские компоненты в HTML на сервере ради быстрой первой отрисовки, но весь их JavaScript всё равно отправляется и гидрируется в браузере. Server Components — другой вид компонентов: они выполняются только на сервере, могут быть асинхронными и обращаться к базам данных или секретам напрямую, а их код никогда не попадает к клиенту. Вниз стримится только их отрендеренный результат, а интерактивные части — это Client Components с пометкой "use client". Они работают вместе: страница Next.js рендерит Server Components, рендерит Client Components в HTML на сервере, а затем гидрирует только клиентские части. В результате JavaScript меньше, а загрузка данных проще.

Q6Что изменилось в React 19 такого, что важно в повседневной работе?

Главное изменение — Actions: асинхронные функции, которые React выполняет в transition, обычно через prop action у формы. Вместе с useActionState, useFormStatus и useOptimistic они убирают большую часть рукописного кода для загрузки, ошибок и оптимистичных обновлений в формах. API use читает промисы и контекст во время рендера, работая вместе с Suspense. ref теперь обычный prop, поэтому forwardRef больше не нужен, а компоненты могут напрямую рендерить теги <title> и <meta>. Server Components и Server Functions стабильны для фреймворков. React 19.2 добавил <Activity> и useEffectEvent, а React Compiler достиг версии 1.0, автоматизировав большую часть мемоизации, которую раньше писали вручную.

Вопросы уровня senior

S1Как бы вы спроектировали управление состоянием для большого React-приложения, над которым работает много команд?

Сначала я классифицирую state, потому что большинство проблем возникает из-за смешения его видов. Серверное состояние проходит через один слой данных — TanStack Query или Server Components — с согласованными соглашениями о query-ключах и правилами инвалидации, так что кэширование единообразно и никто не копирует данные API в store'ы. Состояние URL — фильтры, пагинация и вкладки — живёт в роутере, чтобы им можно было поделиться. Глобальное клиентское состояние держится небольшим и разделено на Zustand store'ы, принадлежащие фичам, с селекторами, и у каждого store есть понятная команда-владелец. Всё остальное остаётся локальным. Взаимодействие между фичами идёт через явные интерфейсы, события или общие хуки, а не через прямое обращение к store другой команды. Компромисс — больше структуры и некоторое дублирование между фичами; это принимается в обмен на то, что команды могут менять свою область, не ломая чужие.

S2Какие проблемы решают Server Components и что они усложняют?

Они решают три реальные проблемы. Размер бандла: код, который только форматирует данные, рендерит markdown или обращается к базе, остаётся на сервере, так что браузеры скачивают и парсят меньше JavaScript. Получение данных: компоненты могут напрямую делать await данных рядом с местом использования, без клиентских водопадов запросов и без API-слоя, построенного только ради UI. Безопасность: секреты и внутренние сервисы никогда не попадают на клиент. Усложняется ментальная модель: разработчики должны знать, на какой стороне выполняется каждый файл, что может пересекать границу (только сериализуемые props) и как в их фреймворке работают кэширование и ревалидация. Отладка охватывает два окружения, а сторонним библиотекам, использующим контекст или эффекты, нужны клиентские обёртки. Кроме того, они привязывают вас к фреймворку, который их реализует. Для высокоинтерактивных приложений за логином клиентское SPA по-прежнему может быть более простым выбором.

S3Загрузка данных в useEffect, TanStack Query или Server Components: как выбрать?

Голый fetch в useEffect — самый слабый вариант по умолчанию: гонки, отмену, кэширование, дедупликацию, повторы и перезагрузку приходится обрабатывать самому, и он создаёт водопады запросов, поэтому я использую его только в тривиальных изолированных случаях. TanStack Query — правильный инструмент для приложений с клиентским рендерингом и высокоинтерактивных данных: дашборды, админские таблицы, бесконечные списки, оптимистичные мутации и фоновое обновление, когда вкладка снова получает фокус. Server Components подходят для данных, нужных для рендера начальной страницы, особенно публичных, важных для SEO или зависящих от секретов, потому что они приходят вместе с HTML и не добавляют клиентского JavaScript. В Next.js-приложении я обычно их комбинирую: Server Components для первого рендера и Server Actions для мутаций, а TanStack Query оставляю для частей, которым нужны живые клиентские обновления, polling или сложное кэширование.

Глоссарий

Fiber
Внутренняя единица работы React: по одному узлу на компонент или элемент, хранящему props, state и отложенные обновления.
Reconciliation
Сравнение нового дерева элементов с предыдущим, чтобы определить минимальные изменения DOM.
Фаза commit
Непрерываемый шаг, на котором React применяет вычисленные изменения к DOM и запускает layout-эффекты.
Transition
Несрочное обновление state, которое React может прервать, чтобы срочные взаимодействия вроде ввода оставались отзывчивыми.
Гидрация
Подключение state и обработчиков событий React к HTML, отрендеренному на сервере.
Hydration mismatch
Первый клиентский рендер отличается от серверного HTML, из-за чего React вынужден перерендерить эту часть.
Граница Suspense
Компонент, который показывает fallback, пока его дочерние элементы ждут код или данные, и стримится независимо.
Виртуализация
Рендер только тех строк списка, которые сейчас видны на экране, вместо всех строк данных.
Rules of React
Правила чистоты, неизменяемости и порядка вызова хуков, благодаря которым оптимизации React и компилятор безопасны.
Peer dependency
Пакет, который библиотека ожидает получить от приложения-хоста, например React, чтобы загружалась только одна копия.

Есть идея продукта?Давайте создадим его.

Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.