Как делать12 октября 2026 г.6 мин чтения
Справочник разработчикаШаг 5 из 8

Справочник хуков React

Коротко

Хук (hook) — функция React с именем на use, которая подключает к компоненту состояние, контекст, ссылки, эффекты или действия. Ниже — встроенные хуки React 19.3 по группам: сигнатура, когда нужен и частая ошибка, плюс короткий пример на TypeScript для каждого ключевого хука. Все примеры прошли tsc, Vitest и запуск в браузере.

СправочникУровень: СреднийСправочник хуков React

После материала вы сможете

  • найти сигнатуру встроенного хука React 19 и понять, какую задачу он решает
  • выбрать между useState и useReducer, useEffect и useLayoutEffect, useTransition и useDeferredValue
  • распознать типичную ошибку хука по симптому и исправить её

Проверено на

React 19.3.0TypeScript 6.0.3, 7.0.2Vite 8.3.4Vitest 5.0.3@testing-library/react 16.3.3eslint-plugin-react-hooks 7.1.1Node.js 24 LTS (24.18.0)

Сверено с документацией: 12 октября 2026 г.

Содержание
  1. 01Правила хуков и карта групп
  2. 02Состояние: useState и useReducer
  3. 03Эффекты: useEffect, useLayoutEffect и родственники
  4. 04Ссылки и контекст: useRef, useContext, use
  5. 05Производительность: useMemo, useCallback, useTransition, useDeferredValue
  6. 06Действия и формы: useActionState, useOptimistic, useFormStatus
  7. 07Прочие: useId, useSyncExternalStore, use(promise)
  8. 08Частые ошибки: симптом → причина

Правила хуков и карта групп

React различает хуки по порядку вызова, поэтому правила строгие: вызывайте хуки только на верхнем уровне компонента или своего хука — до любого раннего return, не внутри условий, циклов, вложенных функций и try/catch. Исключение — use: его можно вызывать в условиях и циклах.

Состояние

Хуки
useState, useReducer
Зачем
компонент «помнит» данные между отрисовками

Контекст

Хуки
useContext, use
Зачем
получить значение от далёкого родителя без пропсов

Ссылки

Хуки
useRef, useImperativeHandle
Зачем
хранить значение без перерисовки, доступ к DOM

Эффекты

Хуки
useEffect, useLayoutEffect, useInsertionEffect, useEffectEvent
Зачем
синхронизация с внешними системами

Производительность

Хуки
useMemo, useCallback, useTransition, useDeferredValue
Зачем
пропустить лишнюю работу, разделить срочные и фоновые обновления

Действия и формы

Хуки
useActionState, useOptimistic, useFormStatus (из react-dom)
Зачем
состояние отправки, мгновенный отклик

Прочие

Хуки
useId, useSyncExternalStore, useDebugValue
Зачем
id для доступности, внешние хранилища, подписи в DevTools
src/PriceWrong.tsx
import { useState } from "react";

// Неправильно: хук после раннего return — порядок вызовов меняется между рендерами
export function Price({ amount }: { amount: number | null }) {
  if (amount === null) return null;
  const [currency, setCurrency] = useState("KZT");

  return (
    <button onClick={() => setCurrency(currency === "KZT" ? "USD" : "KZT")}>
      {amount} {currency}
    </button>
  );
}
Линтер eslint-plugin-react-hooks 7.1 отмечает ошибку: «React Hook "useState" is called conditionally». Тест показал и последствие: выбрали USD, цена пропала и вернулась — снова KZT, состояние молча потерялось. Если хуков несколько, React бросает «Rendered more hooks than during the previous render» или «Rendered fewer hooks than expected».
src/Price.tsx
import { useState } from "react";

// Правильно: хуки — до любого раннего return, порядок вызовов всегда одинаковый
export function Price({ amount }: { amount: number | null }) {
  const [currency, setCurrency] = useState("KZT");
  if (amount === null) return null;

  return (
    <button onClick={() => setCurrency(currency === "KZT" ? "USD" : "KZT")}>
      {amount} {currency}
    </button>
  );
}
Тот же сценарий в тесте сохраняет USD.

Состояние: useState и useReducer

useState

Сигнатура
const [state, setState] = useState(initialState)
Когда
одно значение, от которого зависит отрисовка: поле ввода, флаг, выбранный элемент
Частая ошибка
читать новое значение сразу после setState — состояние это снимок, оно изменится к следующей отрисовке; мутировать объект вместо копии

useReducer

Сигнатура
const [state, dispatch] = useReducer(reducer, initialArg, init?)
Когда
много связанных полей и переходов: корзина, мастер формы, список задач
Частая ошибка
мутировать state в редьюсере или забыть ...state — часть полей станет undefined
src/Counter.tsx
import { useState } from "react";

export function Counter() {
  const [count, setCount] = useState(0);

  function addThree() {
    // updater получает значение с учётом предыдущих вызовов в очереди
    setCount((c) => c + 1);
    setCount((c) => c + 1);
    setCount((c) => c + 1);
  }

  return <button onClick={addThree}>Счёт: {count}</button>;
}
Тест: один клик — «Счёт: 3». С setCount(count + 1) три раза было бы +1: все три вызова видят один и тот же снимок.
src/todos.ts
export type Todo = { id: string; text: string; done: boolean };

export type TodoAction =
  | { type: "added"; id: string; text: string }
  | { type: "toggled"; id: string }
  | { type: "deleted"; id: string };

// чистая функция: (текущее состояние, действие) → новое состояние
export function todosReducer(todos: Todo[], action: TodoAction): Todo[] {
  switch (action.type) {
    case "added":
      return [...todos, { id: action.id, text: action.text, done: false }];
    case "toggled":
      return todos.map((t) => (t.id === action.id ? { ...t, done: !t.done } : t));
    case "deleted":
      return todos.filter((t) => t.id !== action.id);
  }
}
Редьюсер проверен отдельно: входной массив заморожен (Object.freeze) и не меняется. В компоненте: const [todos, dispatch] = useReducer(todosReducer, []) и dispatch({ type: "toggled", id }).
  • Дорогое начальное значение передавайте функцией: useState(() => createRows()), а не useState(createRows()) — иначе оно считается на каждой отрисовке.
  • setX(prev => …), когда новое значение зависит от старого.
  • «Too many re-renders» обычно значит onClick={handleClick()} вместо onClick={handleClick}.
  • Подробно о снимках и обновлениях без мутаций — в уроке про useState.

Эффекты: useEffect, useLayoutEffect и родственники

useEffect

Сигнатура
useEffect(setup, dependencies?)
Когда
синхронизация с внешней системой: подписка, таймер, сокет, виджет не на React
Частая ошибка
вычислять в эффекте то, что можно посчитать при рендере; пропускать зависимости; эффект меняет свою же зависимость — бесконечный цикл

useLayoutEffect

Сигнатура
useLayoutEffect(setup, dependencies?)
Когда
измерить раскладку до отрисовки кадра: позиция подсказки, высота блока
Частая ошибка
ставить вместо useEffect «на всякий случай» — код блокирует отрисовку; на сервере не выполняется

useInsertionEffect

Сигнатура
useInsertionEffect(setup, dependencies?)
Когда
вставка стилей до срабатывания layout-эффектов — для авторов библиотек CSS-in-JS
Частая ошибка
использовать в прикладном коде

useEffectEvent

Сигнатура
const onEvent = useEffectEvent(callback)
Когда
логика внутри эффекта, которая читает свежие пропсы, но не должна перезапускать эффект
Частая ошибка
прятать через него зависимости; вызывать при рендере или передавать в другие компоненты
src/Clock.tsx
import { useEffect, useState } from "react";

export function Clock() {
  const [now, setNow] = useState(() => new Date());

  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 1000);
    return () => clearInterval(id); // очистка: перед повтором и при размонтировании
  }, []); // реактивных значений внутри нет — подписка один раз

  return <time>{now.toLocaleTimeString("ru-RU")}</time>;
}
Тест на поддельных таймерах: через 1000 мс текст меняется; после размонтирования активных таймеров 0. В браузере (dev-сервер Vite, StrictMode) часы идут без предупреждений в консоли.
src/Tooltip.tsx
import { useLayoutEffect, useRef, useState, type ReactNode } from "react";

type Props = { targetTop: number; children: ReactNode };

export function Tooltip({ targetTop, children }: Props) {
  const ref = useRef<HTMLDivElement>(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    // измеряем до того, как браузер нарисует кадр, — без мигания
    setHeight(ref.current!.getBoundingClientRect().height);
  }, []);

  // сверху не помещается — показываем под целью
  const top = height > targetTop ? targetTop + 24 : targetTop - height;

  return (
    <div ref={ref} role="tooltip" style={{ position: "absolute", top }}>
      {children}
    </div>
  );
}
Тест: при высоте 50px подсказка встаёт на top: 50px, при 150px — под цель, на 124px. В Edge: top = 100 − измеренная высота (82px).

Ссылки и контекст: useRef, useContext, use

useRef

Сигнатура
const ref = useRef(initialValue)
Когда
DOM-узел, id таймера, предыдущее значение — всё, что не нужно для отрисовки
Частая ошибка
читать или менять ref.current во время рендера; ждать перерисовки после изменения — её не будет

useImperativeHandle

Сигнатура
useImperativeHandle(ref, createHandle, dependencies?)
Когда
отдать родителю ограниченный набор методов (focus, scrollTo)
Частая ошибка
отдавать методами то, что выражается пропсом: { open, close } вместо isOpen у модального окна

useContext

Сигнатура
const value = useContext(SomeContext)
Когда
тема, язык, текущий пользователь — данные для многих уровней дерева
Частая ошибка
ставить провайдер в том же компоненте или ниже: хук видит только провайдеры выше; новый объект value на каждую отрисовку перерисовывает всех потребителей

use (контекст)

Сигнатура
const value = use(SomeContext)
Когда
то же, но можно после условия или раннего return
Частая ошибка
в Server Components чтение контекста через use не поддерживается
src/SearchBox.tsx
import { useRef } from "react";

export function SearchBox() {
  const inputRef = useRef<HTMLInputElement>(null);

  return (
    <>
      <input ref={inputRef} aria-label="Поиск" />
      <button onClick={() => inputRef.current?.focus()}>Искать</button>
    </>
  );
}
Тест: после клика по кнопке фокус в поле поиска.
src/theme.tsx
import { createContext, use, useContext, useState } from "react";

type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light"); // значение, если провайдера выше нет

export function ThemedButton() {
  const theme = useContext(ThemeContext);
  return <button className={`btn-${theme}`}>Тема: {theme}</button>;
}

function Hint({ show }: { show: boolean }) {
  if (!show) return null;
  const theme = use(ThemeContext); // use() можно вызвать после условия
  return <p>Подсказка для темы {theme}</p>;
}

export function App() {
  const [theme, setTheme] = useState<Theme>("dark");

  return (
    // React 19: сам контекст работает как провайдер
    <ThemeContext value={theme}>
      <ThemedButton />
      <Hint show />
      <button onClick={() => setTheme(theme === "dark" ? "light" : "dark")}>Сменить</button>
    </ThemeContext>
  );
}
Тест: «Тема: dark» и подсказка из use, после клика — «Тема: light»; ThemedButton без провайдера получает значение по умолчанию light. В React 19 ref можно передавать обычным пропсом, а контекст — писать как <ThemeContext value={…}>.

Производительность: useMemo, useCallback, useTransition, useDeferredValue

useMemo

Сигнатура
const value = useMemo(calculateValue, dependencies)
Когда
дорогое вычисление; стабильная ссылка на объект для memo или зависимостей
Частая ошибка
useMemo(() => { a: 1 }, []) вернёт undefined — нужны скобки () => ({ a: 1 }); оборачивать всё подряд

useCallback

Сигнатура
const fn = useCallback(fn, dependencies)
Когда
функция уходит в memo-компонент или в зависимости эффекта
Частая ошибка
без memo у получателя выигрыша нет; без массива зависимостей функция новая на каждой отрисовке

useTransition

Сигнатура
const [isPending, startTransition] = useTransition()
Когда
неблокирующее обновление и Action: смена вкладки, сохранение с индикатором
Частая ошибка
управлять через переход значением поля ввода; обновлять состояние после await без нового startTransition

useDeferredValue

Сигнатура
const deferred = useDeferredValue(value, initialValue?)
Когда
тяжёлая часть интерфейса отстаёт от ввода, а поле остаётся отзывчивым
Частая ошибка
передавать объект, созданный при рендере, — он всегда «новый»; ждать, что он сократит запросы к серверу
src/ProductList.tsx
import { memo, useCallback, useMemo, useState } from "react";

type Product = { id: number; name: string };
type RowProps = { product: Product; onSelect: (id: number) => void };

const Row = memo(function Row({ product, onSelect }: RowProps) {
  return (
    <li>
      <button onClick={() => onSelect(product.id)}>{product.name}</button>
    </li>
  );
});

export function ProductList({ products }: { products: Product[] }) {
  const [query, setQuery] = useState("");
  const [selected, setSelected] = useState<number | null>(null);

  // пересчёт только при смене products или query
  const visible = useMemo(
    () => products.filter((p) => p.name.toLowerCase().includes(query.toLowerCase())),
    [products, query],
  );

  // та же функция между рендерами — memo(Row) не перерисовывает строки
  const handleSelect = useCallback((id: number) => setSelected(id), []);

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Фильтр" />
      <p>Выбран: {selected ?? "—"}</p>
      <ul>
        {visible.map((p) => (
          <Row key={p.id} product={p} onSelect={handleSelect} />
        ))}
      </ul>
    </>
  );
}
Тест: после выбора товара filter не вызывается второй раз, а строки не перерисовываются (геттер name не читается); ввод в фильтр пересчитывает список.
src/Quantity.tsx
import { useState, useTransition } from "react";
import { saveQuantity } from "./api";

export function Quantity() {
  const [qty, setQty] = useState(1);
  const [isPending, startTransition] = useTransition();

  function add() {
    startTransition(async () => {
      const saved = await saveQuantity(qty + 1);
      // обновление после await снова оборачиваем в startTransition
      startTransition(() => setQty(saved));
    });
  }

  return (
    <>
      <button onClick={add} disabled={isPending}>+1</button>
      <output>{isPending ? "Сохраняем…" : qty}</output>
    </>
  );
}
Тест: во время запроса — «Сохраняем…» и кнопка disabled, после ответа — 2. В браузере с настоящей задержкой 300 мс — то же.
src/Search.tsx
import { memo, useDeferredValue, useState } from "react";

const Results = memo(function Results({ items, query }: { items: string[]; query: string }) {
  const found = items.filter((item) => item.includes(query));
  return <ul>{found.map((item) => <li key={item}>{item}</li>)}</ul>;
});

export function Search({ items }: { items: string[] }) {
  const [query, setQuery] = useState("");
  const deferredQuery = useDeferredValue(query); // список догоняет ввод в фоне
  const isStale = query !== deferredQuery;

  return (
    <>
      <input value={query} onChange={(e) => setQuery(e.target.value)} aria-label="Поиск" />
      <div style={{ opacity: isStale ? 0.5 : 1 }}>
        <Results items={items} query={deferredQuery} />
      </div>
    </>
  );
}
Тест: после ввода «ябл» в списке только «яблоко».

Действия и формы: useActionState, useOptimistic, useFormStatus

useActionState

Сигнатура
const [state, dispatchAction, isPending] = useActionState(reducerAction, initialState, permalink?)
Когда
результат отправки формы или действия: ошибки, ответ сервера, флаг ожидания
Частая ошибка
вызывать dispatchAction вне Action (не в action формы и не в startTransition) — ошибка в режиме разработки; ждать параллельности — вызовы идут по очереди

useOptimistic

Сигнатура
const [optimistic, setOptimistic] = useOptimistic(value, reducer?)
Когда
показать результат сразу, пока запрос в пути: лайк, сообщение, товар в корзине
Частая ошибка
вызывать сеттер вне Action — предупреждение, и оптимистичное значение мелькнёт и пропадёт

useFormStatus

Сигнатура
const { pending, data, method, action } = useFormStatus()
Когда
кнопка или поле внутри формы знает, что форма отправляется
Частая ошибка
вызывать в том же компоненте, где <form>: хук видит только родительскую форму
src/SubscribeForm.tsx
import { useActionState } from "react";
import { subscribe } from "./api";

type State = { ok: boolean; message: string } | null;

async function subscribeAction(_prev: State, formData: FormData): Promise<State> {
  const email = String(formData.get("email") ?? "");
  if (!email.includes("@")) return { ok: false, message: "Введите email" };
  try {
    await subscribe(email);
    return { ok: true, message: "Вы подписаны" };
  } catch {
    return { ok: false, message: "Не получилось, попробуйте позже" };
  }
}

export function SubscribeForm() {
  const [state, formAction, isPending] = useActionState(subscribeAction, null);

  return (
    <form action={formAction}>
      <input name="email" aria-label="Email" />
      <button disabled={isPending}>{isPending ? "Отправляем…" : "Подписаться"}</button>
      {state && <p role="status">{state.message}</p>}
    </form>
  );
}
Тест: «abc» — «Введите email»; корректный адрес — «Отправляем…» и disabled, затем «Вы подписаны»; отклонённый запрос — сообщение об ошибке. Сетевой вызов заменён заглушкой.
src/Chat.tsx
import { startTransition, useOptimistic, useState } from "react";
import { sendMessage, type Message } from "./api";

type Shown = Message & { sending?: boolean };

export function Chat() {
  const [messages, setMessages] = useState<Message[]>([]);
  const [shown, addShown] = useOptimistic<Shown[], string>(messages, (state, text) => [
    ...state,
    { id: "pending", text, sending: true },
  ]);

  function send(text: string) {
    startTransition(async () => {
      addShown(text); // сообщение видно сразу, пока идёт запрос
      const saved = await sendMessage(text);
      startTransition(() => setMessages((m) => [...m, saved]));
    });
  }

  return (
    <>
      <button onClick={() => send("Здравствуйте!")}>Отправить</button>
      <ul>
        {shown.map((m) => (
          <li key={m.id}>
            {m.text} {m.sending && "(отправка…)"}
          </li>
        ))}
      </ul>
    </>
  );
}
Тест: сразу после клика — «Здравствуйте! (отправка…)», после ответа — одно сообщение без пометки. В Edge с задержкой 300 мс — то же.

Прочие: useId, useSyncExternalStore, use(promise)

useId

Сигнатура
const id = useId()
Когда
связать label, aria-describedby и поле; одинаковый id при серверной и клиентской отрисовке
Частая ошибка
делать из него key в списке или ключ кэша — их берут из данных

useSyncExternalStore

Сигнатура
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Когда
подписка на внешнее хранилище или API браузера: сеть, размер окна, сторонний store
Частая ошибка
getSnapshot каждый раз возвращает новый объект — ошибка «The result of getSnapshot should be cached»; subscribe внутри компонента — переподписка на каждой отрисовке

use (промис)

Сигнатура
const value = use(promise)
Когда
прочитать результат промиса внутри <Suspense>
Частая ошибка
создавать промис во время рендера — нужен кэшированный промис; оборачивать use в try/catch — ошибки ловит Error Boundary

useDebugValue

Сигнатура
useDebugValue(value, format?)
Когда
подпись своего хука в React DevTools
Частая ошибка
считать в нём дорогое значение без format
src/PasswordField.tsx
import { useId } from "react";

export function PasswordField() {
  const id = useId(); // свой у каждого экземпляра, совпадает на сервере и клиенте

  return (
    <>
      <label htmlFor={`${id}-input`}>Пароль</label>
      <input id={`${id}-input`} type="password" aria-describedby={`${id}-hint`} />
      <p id={`${id}-hint`}>Не меньше 12 символов</p>
    </>
  );
}
Тест: у двух полей на странице разные id, getAllByLabelText("Пароль") находит оба, aria-describedby указывает на подсказку.
src/useOnlineStatus.ts
import { useSyncExternalStore } from "react";

// объявлена вне компонента — React не переподписывается на каждом рендере
function subscribe(callback: () => void) {
  window.addEventListener("online", callback);
  window.addEventListener("offline", callback);
  return () => {
    window.removeEventListener("online", callback);
    window.removeEventListener("offline", callback);
  };
}

export function useOnlineStatus() {
  return useSyncExternalStore(
    subscribe,
    () => navigator.onLine, // снимок на клиенте
    () => true, // снимок при серверном рендеринге
  );
}
Тест: событие offline переключает хук на false. В Edge режим offline через Playwright — то же.
src/Profile.tsx
import { Suspense, use } from "react";
import type { User } from "./api";

function UserName({ userPromise }: { userPromise: Promise<User> }) {
  const user = use(userPromise); // пока промис не выполнен, виден fallback
  return <p>{user.name}</p>;
}

// промис создают вне рендера: в загрузчике, обработчике или Server Component
export function Profile({ userPromise }: { userPromise: Promise<User> }) {
  return (
    <Suspense fallback={<p>Загрузка…</p>}>
      <UserName userPromise={userPromise} />
    </Suspense>
  );
}
Тест: сначала «Загрузка…», после выполнения промиса — имя. Промис в примере создаётся один раз при запуске приложения, поэтому предупреждения о некэшированном промисе нет.

Частые ошибки: симптом → причина

после setState в консоли старое значение

Причина
состояние — снимок текущей отрисовки
Что сделать
использовать значение, которое передали в сеттер, или setX(prev => …)

экран не обновился после изменения объекта

Причина
объект изменён на месте, ссылка та же
Что сделать
создать копию: { ...obj, field }, map, filter

эффект запускается бесконечно

Причина
эффект меняет состояние, от которого сам зависит, или зависит от объекта, созданного при рендере
Что сделать
убрать вычисление из эффекта, вынести объект внутрь эффекта или в useMemo

эффект сработал дважды в разработке

Причина
StrictMode проверяет очистку
Что сделать
дописать функцию очистки, а не отключать проверку

memo не помогает, дочерний компонент перерисовывается

Причина
в пропсах новая функция или объект
Что сделать
useCallback / useMemo для этих пропсов

поле ввода «тормозит» или не печатает

Причина
значение поля обновляется внутри startTransition
Что сделать
поле — обычный setState, тяжёлую часть — через useDeferredValue

состояние сбросилось или ошибка о числе хуков

Причина
хук вызван после условия или раннего return
Что сделать
поднять все хуки наверх, включить eslint-plugin-react-hooks

«A component was suspended by an uncached promise»

Причина
промис для use создаётся при каждом рендере
Что сделать
создавать промис в загрузчике, обработчике или кэшировать

Глубже про состояние — в уроке «React: состояние и useState», про эффекты, гонки запросов и StrictMode — в материале про useEffect. Хуки форм на практике разобраны в уроке о формах и валидации в React.

Как проверено3 · пройденосверено 12 октября 2026 г.
  • запущено, работает

    16 примеров (Counter, todos, TodoList, Clock, Tooltip, SearchBox, theme, ProductList, Quantity, Search, SubscribeForm, Chat, Profile, useOnlineStatus, PasswordField, Price) — RU и EN

    временный Vite-проект, React 19.3.0 (npm view react version): tsc --noEmit на TypeScript 6.0.3 и 7.0.2 — 0 ошибок; vite build 8.3.4 — успешно; Vitest 5.0.3 + jsdom 30.1.2 + @testing-library/react 16.3.3 — 44 теста (22 RU + 22 EN) прошли; eslint-plugin-react-hooks 7.1.1 (recommended) — 0 замечаний

  • запущено, работает

    PriceWrong.tsx (нарушение правил хуков)

    eslint: react-hooks/rules-of-hooks «React Hook "useState" is called conditionally»; Vitest: состояние USD теряется (снова KZT) после amount null → 1000; вариант с двумя хуками бросает «Rendered more hooks than during the previous render» и «Rendered fewer hooks than expected»

  • запущено, работает

    все примеры в браузере

    browser-check.mjs: dev-сервер Vite в StrictMode + Microsoft Edge 155 через Playwright 1.64, RU и EN: счётчик +3, useTransition с реальной задержкой 300 мс, useOptimistic pending → сохранено, use(promise), useActionState, useLayoutEffect top = 100 − высота (82px), часы идут, useSyncExternalStore online/offline через setOffline; 0 ошибок и предупреждений в консоли

«Запущено» — код выполнен во временном проекте или контейнере, вывод в статье взят из прогона. «Частично» — проверено с заглушками вместо внешних сервисов. «Не запускалось» — нужны устройство, аккаунт или ключ; шаги сверены с официальной документацией. Файлы проверочного проекта и логи запусков сохранены в архиве InterLAB.

Расскажите о задаче — мы изучим запрос и свяжемся с вами

Есть задача?
Обсудим и оценим — бесплатно.

Расскажите о продукте или проблеме. Предложим архитектуру, срок и стоимость первой версии после изучения задачи.

  • Оценка объёма и рисков до старта
  • NDA — по вашему или нашему шаблону
  • Старт работ в течение недели

Не присылайте через форму пароли, ключи доступа и исходный код — безопасную передачу доступа обсудим отдельно.

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