Skip to content

Анти-паттерны

Анти-паттерн — решение, которое сначала кажется удобным, но позже приводит к дублированию состояния, утечкам, неверному кэшу или слишком сильной связанности.

Запрос прямо из React-компонента

Плохо:

ts
const response = await api.get("/api/v1/users")
setUsers(response.data)

Компонент теперь отвечает за HTTP, loading, ошибку, отмену и stale response.

Правильно: компонент вызывает предметный метод MobX-store.

ts
void usersStore.fetch()

Импорт API-функции в UI

Плохо:

ts
import { registrationApi } from "../../data/auth/registration/api"

UI становится связан с transport contract. Импортируйте domain store, а API-функцию оставьте внутри data-layer.

Публичный requestHandler

Плохо:

ts
class RegistrationStore {
  readonly requestHandler = createRequestStore(...)
}

Компоненты начнут вызывать execute, читать сырой DTO и обходить mapper.

Правильно:

ts
class RegistrationStore {
  private readonly requestHandler = createRequestStore(...)

  async register(values: RegistrationForm): Promise<User | null> {
    // mapper и предметная логика находятся здесь
  }
}

Выбор API только по HTTP-методу

Неверное правило: «GET всегда FetchStore, POST всегда RequestStore».

Выбор определяется lifecycle:

  • прямой запрос без query cache — RequestStore;
  • ручное кэшируемое чтение — FetchStore;
  • автоматическое реактивное чтение — QueryStore;
  • изменение с callbacks/invalidation — MutationStore;
  • cursor pagination — InfiniteQueryStore.

RequestStore может выполнить GET, а read-only search endpoint через POST может использоваться в FetchStore.

Неполный queryKey

Плохо:

ts
queryKey: () => ["orders"]
queryFn: () => getOrdersApi({ status: this.status })

Все статусы записываются в один cache slot.

Правильно:

ts
queryKey: () => ["orders", "list", { status: this.status }]

Случайное значение в queryKey

Плохо:

ts
queryKey: () => ["users", Math.random()]

Каждое вычисление создаёт новую запись, cache и dedup перестают работать.

Секреты в queryKey

Не помещайте access token, password, cookie или authorization header в ключ. Query key попадает в диагностику, persistence и cross-tab протоколы.

Используйте userId/tenantId partition runtime, но не credential.

Cursor infinite query в queryKey

Плохо:

ts
queryKey: () => ["feed", cursor]

Так каждая страница становится отдельным обычным query.

Правильно: ключ описывает всю ленту, а cursor хранится в pageParams.

ts
queryKey: () => ["feed", "events", { category }]

Новый store при каждом React render

Плохо:

ts
const store = new ProfileStore()

Каждый render создаёт handler и теряет старое состояние.

Правильно:

ts
const [store] = useState(() => new ProfileStore())

или используйте проектный useStoreInstance из Flow разработчика.

Нет dispose при локальном lifecycle

Store, созданный компонентом, должен освобождаться при unmount:

ts
useEffect(() => () => store.dispose(), [store])

Иначе останутся reactions, observers или незавершённые запросы.

Singleton store живёт до завершения приложения и освобождается в application bootstrap.

Автоматический retry неидемпотентной команды

Плохо:

ts
options: {
    retry: 3
}

для endpoint, который создаёт заказ без idempotency key. Повтор может создать несколько заказов.

Используйте retry: false или согласуйте idempotency contract с backend.

Создание Axios client в каждой feature

Это размножает interceptors, refresh flows и настройки cookie.

Создайте publicApi/authApi один раз в src/data/api.ts. Feature API-функции только используют их.

API-функция не принимает Axios config

Плохо:

ts
const getUserApi = (id: string) => api.get(`/users/${id}`)

Store не сможет передать AbortSignal.

Правильно:

ts
const getUserApi = (id: string, config?: AxiosRequestConfig) => {
    return api.get(`/users/${id}`, config)
}

Доверие response.data без проверки

TypeScript generic не проверяет JSON backend во время выполнения.

Для критичных contracts используйте Valibot в API-функции и возвращайте AxiosResponse с проверенным data.

Domain mapper в React

Плохо:

ts
store.execute({ first_name: values.firstName.trim() })

Компонент теперь знает request DTO. Mapper form → DTO должен находиться в data feature рядом со store.

Смешение loading и background refetch

У QueryStore:

  • loading — первичная загрузка без cached data;
  • isFetching — любой network request, включая фоновое обновление.

Если блокировать весь экран по isFetching, UI будет мигать при каждом background refetch.

Широкая invalidation без причины

Плохо:

ts
invalidateQueries: {
    queryKey: []
}

Это может обновить весь cache. Используйте предметные префиксы и точные ключи.

QueryRuntime для одной простой формы

Runtime полезен как общий владелец cache/MF/plugins, но добавляет lifecycle и participant concepts. Для одной изолированной формы прямой RequestStore понятнее.

Несколько QueryRuntime одной сессии

Если каждый feature создаёт собственный runtime, у приложения появляется несколько независимых cache, focus listeners и persistence controllers.

Создавайте runtime на composition root и передавайте scope вниз через application dependencies.

Глобальная переменная runtime в Module Federation

Плохо:

ts
window.queryRuntime = runtime

Remote получает административный root, контракт не типизирован, lifecycle неявный.

Host должен создать participant scope и явно передать его в remote.mount.

Передача root runtime в remote

Remote не должен получать QueryRuntime: root умеет создавать любые scopes и читать diagnostics. Передавайте только IScopedQueryRuntime, ограниченный namespace permissions.

Remote без явных capabilities

Для participantType: "remote" задайте разрешённые query и mutation namespaces. Deny-by-default защищает общий cache от случайного доступа.

Один shared query без definitionId

Если два remote разделяют один query namespace, у definition должен быть стабильный definitionId. Scoped runtime требует его наличие, а FetchStore, QueryStore и InfiniteQueryStore передают id в cache-level conflict check. Если один физический query slot создаётся с другим definitionId, runtime завершает операцию QueryDefinitionConflictError вместо незаметного смешивания разных query definitions.

Персональные данные в localStorage

localStorage читается любым JavaScript-кодом того же origin и не имеет автоматического срока жизни. Не сохраняйте tokens, пароли и чувствительный profile payload. Для небольших разрешённых справочников включайте opt-in meta.persist, scope key и sensitive-data guard.

Persistence без user/tenant partition

Один ключ QUERY_LAYER_CACHE может показать данные предыдущей сессии другому пользователю. Storage key должен включать безопасный hash data partition, а logout должен удалять persisted client.

Одновременные persistence и sync без opt-in политики

Не сохраняйте и не broadcast-ьте все query. Используйте:

ts
meta: {
  persist: true,
  broadcast: true,
}

только для разрешённых данных. Подробнее: Совместное использование плагинов.

HTTP внутри WorkflowStore node

Плохо:

ts
execute: () => axios.post("/checkout")

Workflow обходит MobX-only boundary. Node должен вызывать domain store или query-layer handler, который уже управляет HTTP lifecycle.

Compensation без идемпотентности

Компенсирующий endpoint может быть вызван повторно после reload/recovery. Он должен быть идемпотентным или принимать idempotency key.

Игнорирование ошибки execute()

Store хранит нормализованную ошибку, но Promise всё равно может завершиться с ошибкой. Если вызов запускается без await, используйте осознанную границу:

ts
void store.fetch().catch(reportUnexpectedError)

Не добавляйте пустой catch, который скрывает production-проблемы.