Состояние приложения - state-management в веб разработке

19 февраля 2026
Автор

Олег Марков

Введение

Состояние приложения (state) — это совокупность данных, которые описывают текущее состояние интерфейса и логики в конкретный момент времени. Проще говоря, это ответ на вопрос: «Что сейчас происходит в приложении и что пользователь видит на экране?».

State-management — это подходы, паттерны и инструменты, которые помогают:

  • хранить состояние;
  • изменять его предсказуемо;
  • синхронизировать его с интерфейсом;
  • делить ответственность между частями приложения.

Чем сложнее и «долгоживущее» приложение (SPA, мобильные клиенты, сложные панели администрирования), тем важнее становится то, как именно вы организуете работу с состоянием.

В этой статье вы разберетесь:

  • какие бывают виды состояния;
  • чем локальное состояние отличается от глобального;
  • почему «просто передать пропсы» работает только до какого-то момента;
  • как выглядят классические паттерны state-management;
  • чем отличаются разные подходы — Redux-подобный, реактивный, атомарный, серверный state;
  • какие практические правила помогают держать состояние под контролем.

Смотрите, я буду периодически показывать фрагменты кода на JavaScript / TypeScript и React, но сами принципы применимы и к другим фреймворкам.


Что такое состояние приложения

Локальное против глобального состояния

Для начала давайте разделим состояние по области видимости.

Локальное состояние (UI state)

Это данные, которые нужны только одному компоненту или небольшой его части:

  • открыт ли модальное окно;
  • выбранная вкладка;
  • текст, который пользователь сейчас вводит в поле;
  • состояние раскрытых/свернутых списков.

Пример — простой поиск с локальным состоянием:

// Компонент SearchInput управляет своим локальным состоянием
function SearchInput() {
  // Здесь мы храним текущий текст в поле ввода
  const [query, setQuery] = useState('');

  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    // При каждом вводе символа обновляем локальное состояние
    setQuery(event.target.value);
  };

  return (
    <input
      value={query}            // Поле отображает актуальное значение состояния
      onChange={handleChange}  // При изменении вызываем обработчик
      placeholder="Введите запрос"
    />
  );
}

Локальное состояние проще всего хранить там же, где оно используется. В большинстве фреймворков для этого есть встроенные механизмы (hooks в React, data в Vue, signals в Solid, state в SwiftUI и так далее).

Глобальное состояние (app state)

Это данные, которые нужны в разных частях приложения одновременно:

  • авторизованный пользователь;
  • токен или информация о сессии;
  • текущий выбранный проект / организация;
  • содержимое корзины в интернет-магазине;
  • настройки приложения (тема, язык, права).

Глобальное состояние появляется, когда вы начинаете:

  • передавать одни и те же данные через множество «промежуточных» компонентов;
  • дублировать одни и те же запросы к серверу в разных местах;
  • держать копии одного и того же значения в нескольких компонентах и синхронизировать их вручную.

В этот момент и возникает потребность в организованном state-management.

Виды состояния по источнику и жизненному циклу

Давайте уточним, какие типы состояния обычно выделяют.

UI state (пользовательский интерфейс)

  • то, что не нужно сохранять на сервере;
  • живет только пока открыт экран или компонент;
  • безопасно сбрасывать при обновлении страницы.

Примеры:

  • открыт ли выпадающий список;
  • содержимое неотправленной формы;
  • позиция скролла.

Client state (клиентские данные)

  • данные, которые были получены с сервера или вычислены;
  • могут кэшироваться на клиенте;
  • часто связаны с URL (query-параметры, маршрут).

Примеры:

  • список товаров, загруженный из API;
  • результат фильтрации / сортировки;
  • текущая страница в пагинации.

Server state (серверные данные)

  • «источник правды» находится на сервере;
  • данные могут меняться и на стороне других клиентов;
  • нужно уметь обновлять кэш, инвалидировать, синхронизировать.

Сюда относятся практически все данные из API: пользователи, заказы, посты, статистика и так далее.

Persisted state (сохраняемое состояние)

  • состояние, которое переживает перезагрузку страницы;
  • хранится в localStorage, IndexedDB, cookies, на сервере, в файловой системе.

Примеры:

  • настройки темы (light / dark);
  • «запомнить меня» при логине;
  • черновики текстов.

В реальном проекте вы почти всегда работаете с комбинацией этих типов. Хороший state-management как раз помогает разделить их и работать с каждым типом по правилам, а не в одном «общем мешке».


Основные проблемы без продуманного state-management

Проп-дриллинг и запутанные связи

В какой-то момент вы начинаете писать код примерно так:

  • корневой компонент загружает данные о пользователе;
  • спускает их через 5 уровней вниз;
  • каждый промежуточный компонент просто прокидывает пропсы дальше.

Смотрите, это упрощенный пример для иллюстрации:

// Верхний компонент получает пользователя из API
function App() {
  const [user, setUser] = useState<User | null>(null);

  // Здесь могла бы быть загрузка пользователя с сервера

  return <Layout user={user} />;
}

function Layout({ user }: { user: User | null }) {
  // Layout не использует user, но обязан передать ниже
  return <Sidebar user={user} />;
}

function Sidebar({ user }: { user: User | null }) {
  // Sidebar тоже не использует user, но передает еще глубже
  return <UserPanel user={user} />;
}

function UserPanel({ user }: { user: User | null }) {
  // Только здесь пользователь реально нужен
  if (!user) return <span>Гость</span>;
  return <span>{user.name}</span>;
}

Такая передача параметров через множество уровней называется «prop drilling». Проблемы:

  • каждый промежуточный компонент «знает» о данных, которыми не пользуется;
  • меняется структура — приходится переписывать цепочку пропсов;
  • тестировать и переиспользовать такие компоненты сложнее.

Несинхронизированные копии состояния

Еще одна типичная ситуация: вы храните один и тот же набор данных в нескольких местах.

Например:

  • в одном компоненте храните корзину;
  • в другом — тоже;
  • каждый компонент по-своему меняет массив товаров;
  • иногда состояние расходится, и пользователю показывается разная информация в разных частях UI.

Или:

  • один компонент кеширует результат вызова API;
  • другой — тоже;
  • оба по-разному обновляют данные;
  • вы ловите трудноуловимые баги.

Сложность отладки и непредсказуемость

Если состояние изменяется:

  • в произвольных местах кода;
  • без централизованной логики;
  • через прямое мутирование объектов;

то рано или поздно становится сложно ответить на вопросы:

  • «Почему компонент перерисовался в этот момент?»;
  • «Кто изменил это значение?»;
  • «Почему при одном и том же действии я получил два разных результата?».

State-management как дисциплина и набор инструментов как раз и отвечает на эти проблемы.


Базовые принципы state-management

Единственный источник правды (Single Source of Truth)

Идея простая: каждое значение в приложении должно иметь одно «главное» место хранения. Остальное — это производные данные (селекторы, вычисления, кеши, копии для локального использования).

Например:

  • корзина хранится в одном сторе;
  • компоненты получают ее через подписку или хук;
  • фильтрованные варианты корзины не хранятся отдельно — они вычисляются при необходимости.

Это сильно снижает риск того, что в разных местах приложения появятся разные версии одних и тех же данных.

Неизменяемость (immutability) и предсказуемость

Еще одна важная идея — не мутировать состояние напрямую, а создавать новые версии.

Смотрите, пример с и мутируемым и неизменяемым подходом:

// Прямое мутирование - может приводить к скрытым багам
const cart = ['apple', 'banana'];
cart.push('orange'); // Мы изменили исходный массив

// Неизменяемый подход - создаем новый массив
const cart2 = ['apple', 'banana'];
const newCart = [...cart2, 'orange']; // Исходный массив не меняем

Почему это важно:

  • легче отслеживать, что именно поменялось;
  • проще реализовать откат (undo / redo);
  • проще оптимизировать рендеры (поверхностное сравнение объектов);
  • легче писать чистые функции, которые не имеют побочных эффектов.

Явная модель изменений (actions, events)

Многие библиотеки state-management предлагают записывать изменения состояния в форму действий (actions, events):

  • «userLoggedIn»;
  • «itemAddedToCart»;
  • «todoCompleted».

Давайте разберем простой пример с action-ами на основе редьюсера:

type CartAction =
  | { type: 'ADD_ITEM'; payload: string }
  | { type: 'REMOVE_ITEM'; payload: string };

function cartReducer(state: string[], action: CartAction): string[] {
  switch (action.type) {
    case 'ADD_ITEM':
      // Возвращаем новый массив с добавленным товаром
      return [...state, action.payload];
    case 'REMOVE_ITEM':
      // Возвращаем новый массив без удаляемого товара
      return state.filter(item => item !== action.payload);
    default:
      return state;
  }
}

Зачем это нужно:

  • все изменения проходят через одну «воротину»;
  • легко логировать actions и понимать, что происходило;
  • можно подключать инструменты для отладки (time-travel, devtools).

Подходы к управлению состоянием

Давайте посмотрим на несколько распространенных подходов в современных фронтенд-приложениях.

Локальное состояние + поднятие состояния (lift state up)

Это самый базовый и встроенный в фреймворки подход:

  • каждый компонент управляет своим состоянием;
  • если нескольким компонентам нужны одни и те же данные — поднимаем состояние выше в дерево;
  • при необходимости пробрасываем через пропсы.

Пример: общая строка поиска для двух списков.

// Родитель хранит общее состояние - поисковый запрос
function Dashboard() {
  const [query, setQuery] = useState('');

  return (
    <>
      <SearchBar query={query} onChange={setQuery} />
      <UsersList query={query} />
      <OrdersList query={query} />
    </>
  );
}

// Поле ввода не хранит состояние - работает как "контролируемое"
function SearchBar({
  query,
  onChange,
}: {
  query: string;
  onChange: (value: string) => void;
}) {
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    // Сообщаем родителю об изменении строки поиска
    onChange(event.target.value);
  };

  return <input value={query} onChange={handleChange} />;
}

Плюсы:

  • минимальная сложность;
  • не нужны дополнительные библиотеки;
  • хорошо работает на малых и средних проектах.

Минусы:

  • вы быстро приходите к prop drilling;
  • трудно переиспользовать компоненты в других ветках дерева.

Контекст (context) или DI-контейнер

Чтобы избежать prop drilling, многие фреймворки предлагают механизм контекста (Context в React, provide/inject в Vue, Dependency Injection в Angular).

Суть:

  • создается объект-контейнер со значениями;
  • «провайдер» кладет в контекст значения;
  • «потребители» могут брать значения из контекста в любом месте поддерева.

Смотрите пример с контекстом авторизации:

// Создаем контекст для пользователя
const AuthContext = createContext<User | null>(null);

// Провайдер получает пользователя извне и делает его доступным детям
function AuthProvider({ user, children }: { user: User | null; children: React.ReactNode }) {
  return (
    <AuthContext.Provider value={user}>
      {children}
    </AuthContext.Provider>
  );
}

// Хук-обертка для удобства использования контекста
function useAuth() {
  const user = useContext(AuthContext);
  // Здесь можно добавить дополнительные проверки
  return user;
}

// Любой компонент внутри AuthProvider может получать user
function UserPanel() {
  const user = useAuth();

  if (!user) return <span>Гость</span>;
  return <span>{user.name}</span>;
}

Контекст — удобный инструмент, но он сам по себе не решает вопросы:

  • как менять состояние;
  • как обрабатывать побочные эффекты;
  • как хранить сложные данные.

Централизованный стор и паттерн Flux / Redux

Когда состояние становится большим и сложным, многие переходят к централизованным сторам:

  • один (или несколько) глобальных сторов;
  • состояние хранится в объекте;
  • изменения происходят только через actions;
  • редьюсеры описывают, как actions трансформируют state.

Покажу вам сокращенную версию ручной реализации похожего подхода:

type State = {
  count: number;
};

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' };

type Listener = (state: State) => void;

class Store {
  private state: State;
  private listeners: Listener[] = [];

  constructor(initialState: State) {
    // Здесь мы инициализируем начальное состояние
    this.state = initialState;
  }

  // Метод для чтения текущего состояния
  getState() {
    return this.state;
  }

  // Подписка на изменения состояния
  subscribe(listener: Listener) {
    this.listeners.push(listener);
    // Возвращаем функцию для отписки
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  // Основной метод - обработка action-ов
  dispatch(action: Action) {
    // Здесь мы описываем, как каждое действие меняет состояние
    switch (action.type) {
      case 'INCREMENT':
        this.state = { ...this.state, count: this.state.count + 1 };
        break;
      case 'DECREMENT':
        this.state = { ...this.state, count: this.state.count - 1 };
        break;
    }

    // После изменения оповещаем всех подписчиков
    this.listeners.forEach(listener => listener(this.state));
  }
}

// Создаем стор с начальным значением
const store = new Store({ count: 0 });

// Пример использования
store.subscribe(state => {
  // Этот коллбэк будет вызван при каждом изменении состояния
  console.log('Count changed to', state.count);
});

// Вызываем изменения через dispatch
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });

Библиотеки вроде Redux, NgRx, Akita, Zustand, Pinia реализуют похожие идеи, но с разной «обвязкой» и синтаксисом.

Плюсы централизованного стора:

  • один источник правды;
  • четкий поток данных (однонаправленный поток);
  • мощные инструменты отладки;
  • удобно работать в больших командах.

Минусы:

  • избыточность для простых задач;
  • дополнительная «церемония» (actions, редьюсеры, типизация);
  • необходимость дисциплины при проектировании.

Реактивные и сигнальные подходы

Другой популярный класс решений — реактивные библиотеки и сигналы:

  • RxJS;
  • MobX;
  • signals в различных фреймворках.

Идея:

  • вы объявляете «наблюдаемые» значения (observables, signals);
  • описываете зависимости и вычисляемые значения;
  • при изменении базового значения все зависимые автоматически обновляются.

Давайте разберем упрощенный пример с «сигналами» (псевдокод):

// Здесь создаем базовые сигналы
const price = signal(100);     // Базовая цена
const quantity = signal(2);    // Количество

// Создаем вычисляемый сигнал, зависящий от двух базовых
const total = computed(() => price.value * quantity.value);

// Подписываемся на изменения вычисляемого значения
effect(() => {
  console.log('Total changed:', total.value);
});

// Меняем исходные значения
price.value = 120;   // total обновится автоматически
quantity.value = 3;  // и снова пересчитается

Плюсы:

  • декларативность зависимостей;
  • естественная модель для сложных форм, таблиц, вычислений;
  • меньше «ручного» управления рендерами.

Минусы:

  • сложнее отлаживать, если переборщить с магией;
  • необходимо понимать, как устроено отслеживание зависимостей.

Server State и библиотеки вроде React Query / SWR

Отдельная категория — управление серверным состоянием на клиенте. Это:

  • загрузка данных;
  • кеширование;
  • повторные запросы;
  • обработка ошибок;
  • синхронизация при фокусе вкладки и смене сети.

Инструменты вроде React Query, SWR, Apollo Client предлагают сделать server state «первоклассным» гражданином.

Пример с React Query:

// Хук useQuery сам управляет кешем и жизненным циклом запроса
function UsersList() {
  const { data, isLoading, error } = useQuery({
    queryKey: ['users'],          // Ключ в кеше
    queryFn: fetchUsersFromApi,   // Функция запроса к серверу
  });

  if (isLoading) return <span>Загрузка...</span>;
  if (error) return <span>Ошибка загрузки</span>;

  return (
    <ul>
      {data?.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Здесь главная идея: не каждый серверный ответ нужно класть в «глобальный стор». Лучше использовать специализированный слой, который понимает особенности server state.


Как выбрать подход к state-management

Разделите состояние по уровням

Первый практический шаг — ответить на вопрос: какому уровню принадлежит конкретное состояние?

  • только для одного компонента → локальное состояние;
  • нужно нескольким соседним компонентам → поднятие состояния в ближайший общий родитель;
  • нужно многим далеким компонентам → контекст или стор;
  • это данные с сервера → библиотека для server state;
  • нужно сохранять между сессиями → слой персистентности (localStorage, IndexedDB).

Частая ошибка — «все в глобальный стор, так удобнее». На практике:

  • UI state лучше оставлять локальным;
  • server state — отдавать на откуп специализированным инструментам;
  • глобальный стор использовать для действительно бизнес-важных и кросс-срезных данных.

Начинайте с простого и эволюционируйте

Обычно стратегию можно описать так:

  1. Начинаете с локального состояния и поднятия.
  2. Сталкиваетесь с prop drilling и дублированием — вводите контекст.
  3. Осознаете, что часть данных — это server state, подключаете React Query или аналог.
  4. Понимаете, что конфигурация приложения и бизнес-данные растут — добавляете легковесный глобальный стор (Zustand, Pinia, NgRx).
  5. При необходимости — разделяете сторы и выделяете доменные модули.

Отделяйте доменную логику от UI

Удобная практика — хранить бизнес-логику и состояние отдельно от компонентов. Смотрите, пример:

// cartStore.ts - модуль работы с корзиной

// Здесь определяем тип товара и состояние корзины
type CartItem = { id: string; name: string; price: number };

type CartState = {
  items: CartItem[];
};

// Функции для работы с состоянием корзины
function addItem(state: CartState, item: CartItem): CartState {
  return { ...state, items: [...state.items, item] };
}

function removeItem(state: CartState, id: string): CartState {
  return { ...state, items: state.items.filter(i => i.id !== id) };
}

function calcTotal(state: CartState): number {
  return state.items.reduce((sum, item) => sum + item.price, 0);
}

А в UI вы работаете уже поверх этих функций, не зная деталей реализации:

// CartView.tsx - компонент отображения корзины

function CartView({ state, onAdd, onRemove }: {
  state: CartState;
  onAdd: (item: CartItem) => void;
  onRemove: (id: string) => void;
}) {
  // Получаем итоговую сумму через отдельную функцию
  const total = calcTotal(state);

  return (
    <div>
      <ul>
        {state.items.map(item => (
          <li key={item.id}>
            {item.name} - {item.price}
            <button onClick={() => onRemove(item.id)}>Удалить</button>
          </li>
        ))}
      </ul>
      <div>Итого: {total}</div>
      {/* Здесь могла бы быть кнопка "Добавить товар" */}
    </div>
  );
}

Такой подход:

  • упрощает тестирование (логику можно тестировать без UI);
  • позволяет переиспользовать ту же логику в другом интерфейсе (например, мобильном);
  • снижает связанность кода.

Практические рекомендации по работе с состоянием

Держите состояние максимально близко к месту использования

Локальное состояние дешевле по «когнитивной цене». Чем ближе значение к компоненту, который его использует, тем проще понять, что происходит.

Пример:

  • флаг «открыто ли модальное окно» лучше хранить в родителе этого модального окна, а не в глобальном сторе;
  • состояние отдельного поля формы — внутри компонента формы, а не в общем контексте приложения.

Не делайте из URL «антипаттерн»

URL — это тоже источник состояния (route state):

  • текущий экран;
  • параметры фильтрации;
  • текущая страница.

Но:

  • не стоит дублировать одни и те же данные и в URL, и в сторе, и в локальном состоянии;
  • лучше сделать функцию, которая читает состояние из URL, и использовать ее как «источник правды» для конкретного сценария.

Пример (идея):

  • вместо того чтобы хранить page в сторе, берите его из query-параметра;
  • при смене страницы — обновляйте URL, а все остальные компоненты получают актуальное значение из одного места.

Относитесь к server state отдельно

Важный практический совет:

  • не пытайтесь «забетонировать» данные, полученные с сервера, в глобальном сторе навсегда;
  • помните, что на сервере эти данные могут поменяться в любой момент;
  • учитывайте кеш, инвалидизацию, фоновые обновления.

Гораздо проще использовать специализированные библиотеки, которые:

  • умеют повторять запрос при ошибке;
  • обновляют данные при фокусе вкладки;
  • синхронизируют кеш при мутациях.

Минимизируйте количество «глобального»

Глобальное состояние:

  • сложнее контролировать;
  • сложнее тестировать;
  • влияет на большее количество компонентов.

Хорошая эвристика: если данные можно локализовать — локализуйте.

Полезно периодически задавать себе вопросы:

  • «А действительно ли это должно быть глобальным?»;
  • «Можно ли ограничить область видимости этим модулем / страницей?»;
  • «Что будет, если я удалю это из стора и положу ближе к компоненту?».

Заключение

Состояние приложения — это не только «переменные внутри компонентов», но и:

  • модель данных, с которой вы работаете;
  • способ, которым вы синхронизируете UI и бизнес-логику;
  • архитектурное решение, влияющее на масштабируемость проекта.

Вы посмотрели:

  • какие бывают типы состояния — локальное, глобальное, UI state, client state, server state, persisted state;
  • какие проблемы появляются без продуманного подхода — prop drilling, дублирование и рассинхронизация данных;
  • какие базовые принципы помогают — единственный источник правды, неизменяемость, явные события изменений;
  • какие подходы к state-management существуют — локальное состояние, контекст, централизованные сторы, реактивные библиотеки, server state библиотеки;
  • какие практические правила уменьшают сложность — держать состояние ближе к месту использования, разделять серверные и клиентские данные, минимизировать глобальное состояние.

State-management — это не «про выбор библиотеки», а про то, как вы мыслите о данных и их жизненном цикле. Библиотека лишь фиксирует и усиливает выбранный вами подход. Если принципы выстроены, конкретный инструмент обычно подобрать уже несложно.


Частозадаваемые технические вопросы по теме и ответы

Как безопасно хранить состояние в localStorage и не ломать SSR

При серверном рендеринге объект window недоступен. Решение:

  1. Обращайтесь к localStorage только внутри эффектов, которые выполняются на клиенте.
  2. Делайте защитные проверки.

Пример:

// Функция безопасно читает значение из localStorage
function loadFromLocalStorage(key: string): string | null {
  // Проверяем, что код выполняется в браузере
  if (typeof window === 'undefined') return null;

  try {
    return window.localStorage.getItem(key);
  } catch {
    // В случае ошибки возвращаем null
    return null;
  }
}

Используйте эту функцию внутри useEffect или аналогов в других фреймворках.


Как избежать лишних перерисовок при использовании глобального стора

Основные шаги:

  1. Разбивайте стор на «срезы» (slices), чтобы подписываться только на нужную часть.
  2. Используйте селекторы, которые возвращают только необходимые данные.
  3. Применяйте мемоизацию и поверхностное сравнение.

Пример с селектором:

// Селектор получает из состояния только нужное поле
const selectUserName = (state: RootState) => state.user.name;

// Компонент подписывается не на весь user, а только на имя
function UserName() {
  const name = useSelector(selectUserName); // Здесь useSelector сравнит старое и новое значение
  return <span>{name}</span>;
}

Так вы уменьшаете количество компонентов, которые реагируют на изменения в сторе.


Как правильно хранить ошибки и состояния загрузки в сторе

Рекомендуется:

  1. Для каждой единицы server state хранить «триаду»:
    • data;
    • isLoading;
    • error.
  2. Не смешивать ошибки сети и бизнес-ошибки.

Структура:

type ApiState<T> = {
  data: T | null;
  isLoading: boolean;
  error: string | null;
};

В редьюсере:

  • при запросе: isLoading = true, error = null;
  • при успехе: data = payload, isLoading = false;
  • при ошибке: error = message, isLoading = false.

Как организовать state-management для больших форм

Рабочий подход:

  1. Держите «сырые» значения полей локально в компоненте формы.
  2. В глобальный стор отправляйте только финальный результат (после сабмита).
  3. Для валидации используйте отдельный слой (например, schema валидации).

Мини-структура для формы:

type FormState = {
  values: Record<string, string>;   // Значения полей
  errors: Record<string, string>;   // Сообщения об ошибках
  touched: Record<string, boolean>; // Флаги "поле было тронуто"
};

Так вы не загружаете глобальный стор временными данными и упрощаете жизнь при работе с черновиками.


Как мигрировать с «самодельного» стора на библиотеку

Пошаговый план:

  1. Выделите слой доступа к состоянию (функции getState, setState, subscribe).
  2. Замените прямые обращения к «самодельному» стору на эти функции.
  3. Внедрите библиотеку (Redux, Zustand, Pinia и так далее), но сохраните интерфейс слоя.
  4. Постепенно переписывайте внутреннюю реализацию, не трогая остальной код.

Таким образом, UI и бизнес-логика не зависят напрямую от конкретной библиотеки state-management, и миграция проходит поэтапно, без «большого взрыва».

Стрелочка влевоСтилизация в Feature-Sliced Design - практическое руководствоМокирование данных - базовые и продвинутые техники для надежных тестовСтрелочка вправо

Все гайды по Feature-sliced_design

Открыть базу знаний

Отправить комментарий