Тема
Анти-паттерны
Анти-паттерн — решение, которое сначала кажется удобным, но позже приводит к дублированию состояния, утечкам, неверному кэшу или слишком сильной связанности.
Запрос прямо из 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 = runtimeRemote получает административный 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-проблемы.