Тут я расскажу о том, как я впервые с нуля поднимал проект на React, используя связку FSD, TanStack Router, TanStack Query и Effector — и как мы всё это далее подружили подружили или нет.
Сразу оговорюсь:
Проектом занимается команда из 4х разработчиков, но архитектурный старт, выбор технологий и базовая структура — легли на меня. Это был мой первый опыт в такой роли: отвечать не просто за компоненты или страницы, а за фундамент проекта.
А так же, это моя первая статья. Не претендую на истину в последней инстанции, но надеюсь, кому‑то мой опыт будет полезен и палками бить сильно не будете.
Описание проекта
Проект представляет собой админ‑панель, поддерживающую несколько более крупных продуктов. Основные функции: управление сертификатами, правами доступа, ролями, настройками.
В качестве UI‑библиотеки выбран Ant Design, кастомизированный под нужды проекта.
Формально мы переписывали существующее Angular 12-приложение, но фактически вся архитектура создавалась заново: маршрутизация, состояние, взаимодействие с API.
Почему именно TanStack Query, Router и Effector?
TanStack Query был выбран из‑за его мощности в работе с запросами: встроенное кеширование, повторные попытки запросов, фоновая подгрузка, бесконечная пагинация — всё это из коробки и без костылей. Благодаря кешу снижается количество реальных сетевых запросов (количество пользователей системы — примерно 9000 человек).
Некоторые фичи этого решения будут подробнее показаны ниже, в том числе пример с useInfiniteQuery
.
Далее логично за TanStack Query пришёл и TanStack Router, потому что они отлично стакаются вместе: можно грузить данные прямо во время перехода между страницами, используя loader
прямо в конфигурации маршрута. Также из коробки — различные фичи по типу beforeLoad
и валидации параметров маршрута.
Effector же я подключил не для хранения данных с бэка — этим у нас занимается TanStack Query как основной асинхронный стейт‑менеджер.
Effector нужен для другого: упростить взаимодействие между разнесёнными компонентами, например, когда форма где‑то глубоко, а кнопка «Сохранить» — наверху. Как это работает — покажу ниже в статье.
Такой стек дал гибкость, контроль и внятное разделение зон ответственности между слоями — и при этом, как мне кажется, не перегружен лишним.
Структура проекта и организация окружения
Да да, в качестве архитектурного фундамента выбран Feature‑Sliced Design (FSD)

В слое app
размещены глобальные компоненты, такие как Layout (Header
, Sidebar
) и провайдеры окружения.
— Провайдер для AntD‑темы
— Провайдер QueryClient
;
— Провайдер маршрутизации.
Провайдеры
Каждый провайдер вынесен в отдельный компонент.


Для TanStack Router, выбор пал на code‑based подход в силу использования FSD.
Вообще с TanStack Router всё было крайне легко, очень удобный инструмент как мне показалось, только у ребят на соседнем проекте были проблемы с IDE когда включали строгую типизацию.
У меня же с ним только хорошие ассоциации даже тёплые воспоминания о роутинге Angular.
Работа с API
Бэкенд построен на Java, примерно 300+ эндпоинтов, описанных в OpenAPI спецификации. На первый взгляд, всё хорошо — можно сгенерировать типизированный клиент. Но:
Многие эндпоинты дублировались (
getCard
,getCard_1
,getCard_2
);Контракты нестабильны: где‑то
null
, где‑то"0"
, где‑то массив из одного объекта;ref
‑поля не использовались должным образом — никакой вложенной типизации, по фактуany
.
Мы пробовали разные генераторы: openapi-typescript
, orval
, heyapi
. Все упирались в несовместимость или избыточную сложность.
В итоге остановились на swagger-typescript-api
— максимально простой и предсказуемый инструмент.
Плюсы:
шаблонная генерация без сюрпризов;
легко настраивается;
даёт основу, которую можно доработать вручную.
Минусы:
типизация очень поверхностная;
отсутствуют связи между сущностями;
нужно самостоятельно следить за согласованностью моделей и кешей.
(Примерно в этот момент захотелось написать свой генератор. Не для продакшена, а просто чтобы лучше понять, как всё это устроено.)
TanStack Query и ключи кеширования
Эта часть, пожалуй, самая важная — именно из‑за неё я и решил написать статью.
Как только мы внедрили TanStack Query, сразу встал вопрос: как вести ключи кеширования (queryKey
)?
Первая реализация казалась жизнеспособной
На старте мы пошли по пути централизованного объекта QUERY_KEYS
, где ключи определялись через ApiEntities
и параметры методов API:

Идея была в том, чтобы все queryKey
шли через один объект и были типизированы через параметры конкретных методов API. Формально это работало, но на практике:
Ключи были непрозрачными — без знания API или
ApiEntities
сложно понять, что за данные кешируются;Параметры были слабо читаемы (
...params
), особенно если объект фильтров сложный;Инвалидация требовала знания того, как именно был построен ключ — универсального интерфейса не было.
В какой‑то момент мы поняли: такая схема слишком неудобна и неповоротлива.
Финальная реализация: фабрика ключей и разделение по сущностям
Покопавшись в интернетах и не найдя хороших материалов по теме организации queryKey
в TanStack Query, я собрал следующий подход на основе накопленных наблюдений.
Поскольку ключи лучше передавать в виде массива строк:
const info = useQuery({ queryKey: ['todos'], queryFn: fetchTodoList })
// или
const info = useQuery({ queryKey: ['todos', 'completed'], queryFn: fetchCompletedTodoList })
// и так может быть ещё несколько запросов с началом queryKey: ['todos', ...]
Так становится удобно инвалидировать кеш по этим значениям. Например, если я захочу инвалидировать весь кеш, связанный с todos
, достаточно написать:
queryClient.invalidateQueries({ queryKey: ['todos'] })
И все ключи, начинающиеся с todos
, будут инвалидированы. Это очень удобно.
Но я пошёл немного дальше и сделал фабрику генерации ключей:
export function createQueryKey<T extends unknown[]>(namespace: string, ...args: T) {
return [namespace, ...args] as const;
}
В итоге для каждого типа запросов я завёл константу с ключами:
import { createQueryKey } from 'shared/api/queryKeys/createQueryKey';
export const userQueryKeys = {
all: () => createQueryKey('users'),
byId: (id: string) => createQueryKey('users', id),
card: (id: string) => createQueryKey('users', id, 'card'),
byFilters: (filters: UserFilters) =>
createQueryKey('users', 'filters', JSON.stringify(filters)),
};
Это позволяет удобно использовать ключи в хуках с запросами:
export const useUserCard = (id: string, enabled = true) =>
useQuery({
queryKey: userQueryKeys.card(id),
queryFn: () => getUserCard({ id }),
enabled: enabled && !!id,
});
А инвалидация теперь выглядит так:
queryClient.invalidateQueries({ queryKey: userQueryKeys.all() });
queryClient.invalidateQueries({ queryKey: reestrQueryKeys.byComplexFilter({}) });
Если мы кешируем значение по фильтрам:
export type ReestrUsersComplexFilter = {
status?: string;
roles?: string[];
dateFrom?: string;
dateTo?: string;
search?: string;
};
byComplexFilter: (filters: ReestrUsersComplexFilter) =>
createQueryKey('reestrUsers', 'complex', JSON.stringify(filters));
Для сложных объектов используйте
JSON.stringify
— так ключ будет однозначным (если порядок ключей всегда одинаков).
После всех этих нововведений, работа с ключами в проекте стала куда более предсказуемой — код, структура, подходы к запросам и кешированию теперь заданы довольно чётко. И легко инвалидировать кеш как локально по определённым запросам, так и глобально по модулю.
Effector и зачем я его вообще взял
Да, TanStack Query сам по себе является асинхронным стейт‑менеджером, но Effector здесь не просто так — он решает конкретную задачу:
Мне нужно было, чтобы компоненты, не связанные напрямую по иерархии, могли взаимодействовать.
Самый простой пример:
Я использую AntD форму, которая расположена глубоко в компоненте. А кнопку «Сохранить» хочу разместить в Header
родительского роута — или вообще вынести в другой виджет.
То есть:
Кнопка «Сохранить» — в одном месте;
Вызов мутации формы — в другом, внутри формы.
Передавать коллбеки пропсами — невозможно. Использовать контекст — громоздко и неявно.
А вот Effector дал простой и удобный способ: создать event
, на который подписана форма, и триггерить его из кнопки.
Таким образом, Effector стал связующим звеном между UI‑блоками, особенно в сложных сценариях с вложенными роутами и формами.
Итоги
TanStack Router и TanStack Query отлично работают в связке поскольку они в одной набирающей популярность экосистеме TanStack. Их API и подходы к состоянию, загрузке данных и потоку данных очень хорошо сочетаются.
Один из главных плюсов: возможность запрашивать данные прямо в конфигурации маршрутов.
const postsLayoutRoute = createRoute({
getParentRoute: () => rootRoute,
path: 'posts',
loader: ({ context: { queryClient } }) =>
queryClient.ensureQueryData(postsQueryOptions),
}).lazy(() =>
import('./posts.lazy').then((d) => d.Route)
)
const postsIndexRoute = createRoute({
getParentRoute: () => postsLayoutRoute,
path: '/',
component: PostsIndexRouteComponent,
})
Загрузка данных заранее, до рендера компонента, происходит типобезопасно, синхронно с маршрутизацией и без лишних ручных проверок.
Сам по себе TanStack Query — это мощный инструмент для работы с данными. Он покрывает практически все потребности, включая:
кеширование,
повторные попытки,
загрузку по требованию,
фоновое обновление,
пагинацию и бесконечную прокрутку
// Простой пример с бесконечной пагинацией
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetching,
isFetchingNextPage,
status,
} = useInfiniteQuery({
queryKey: ['projects'],
queryFn: fetchProjects,
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor,
})
А если бэкенд не предоставляет nextCursor
, и приходится рассчитывать это вручную — TanStack Query тоже справляется

И всё это без надстроек в виде сложных компонентов для виртуального скролла и тд.
Важно сказать что не стоит пытаться класть результаты запроса из TanStack Query в сторы Effector, это плохая идея, если сильно хочется то лучше взять Farfetched, но тут есть уже свои моменты.
Комментарии (18)
markelov69
20.06.2025 20:13используя связку FSD, TanStack Router, TanStack Query и Effector
Вы конечно собрали комбо из самых худших подходов. Зачем?
Классическая архитектура, React, MobX, SCSS + CSS modules - идеальный выбор ещё с 2016 года и по сей день.
Для роутинга либо самописное решение. Из готовых, всё гуано.
Для АПИ запросов самописная обертка, которая использует axios. Из готовых, всё гуано.
Для работы с формами и валидацией, то же самописное решение. Из готовых, всё гуано.
Более того, React нужно использовать исключительно как View слой, а не как средство для управления состоянием и т.п.
P.S. React way === Худший путь.VadimLatypov Автор
20.06.2025 20:13Очень субъективно и с устаревшим взглядом, так можно и на ванильном JS писать без библиотек, все на самочинном, не желание принять развитие окружения чревато.
Выбирать mobx когда команда его не знает равносильно выстрелу в ногу, использовать модули на scss в 2025м году, когда css убил препроцессор в после ~21го года - сомнительное решение
Зачем тратить время на возможно кривые самописные решения в заказной разработке где сжатые сроки на релизы?
С такими суждениями бы окунуться в актуальные решения, имхо, собственно как и ваше
DmitryKazakov8
20.06.2025 20:13Ну Mobx не то чтобы сложен. Пишем в обычном стиле где сторы и ViewModel - это классы, добавляем makeAutoObservable - получаем автоматический точечный ререндер компонентов при изменении только тех свойств, которые используются в компоненте.
Но тут согласен, от команды многое зависит - если совсем никто не пользовался, то обучение будет дольше, а если хоть один - то очень быстро других обучит.
А вот чем CSS Modules и scss не угодили?
Модули позволяют исключить пересечение стилей между компонентами, дают быстрый переход на место объявления, позволяют настраивать именование класса (например включать путь
src-components-layouts-layoutWrapper
), что создает ясный идентификатор для компонентов. Для e2e тестов не подойдет, но для дебага - прекрасно, сразу видно кнопку в каком из 100 компонентов, использующих кнопки, нажал пользователь и получил ошибку. Также при желании можно настроить генерацию d.ts файлов из стилей, чтобы найти все неиспользуемые классы (на постоянной основе - нежелательно, но в качестве плановой оптимизации раз в 3 месяца - отлично).Scss - это миксины, nesting (вложение одних классов в другие), математика и циклы, удобные импорты. Не обязательно использовать именно препроцессоры - можно использовать сразу постпроцессоры (PostCSS) с соответствующим интерпретатором. Во многих проектах постпроцессор и так используется (для автопрефикса и оптимизации например), и добавление scss не сильно ухудшит перфоманс сборки, но добавит удобный DX.
Ну, про FSD, TanStack Router, TanStack Query и Effector я согласен с предыдущим комментатором, что сейчас это самое неэффективное, и это никак не связано с годом их выпуска. Но популярных альтернатив лучше и правда мало, и если сжатые сроки - то можно и затянуть в проект, особенно если команда хорошо с ними знакома.
VadimLatypov Автор
20.06.2025 20:13Не, я не против mobx, тут да, от команды зависит
Я использую как раз модули, тут тейк был про scss именно, что он по факту не нужен, сейчас с актуальными возможностями css базового (включая модули) без препроцессоров можно обойтись в 99% случаев
Миксины фича, но не всегда нужна, можно и без них, тем более ради одних миксинов замедлять сборку, все равно в css обычный по итогу перейдет
С вложенностью сложно, у нас в командах почему то не любят вложенность классов scss, если даже используют, то пишут всё в строку.
По факту надобности сильной в scss нет, ипорты есть и базовом css.
На момент написания стати этот стек довольно неплох, с одним НО, в effector в этом решении используется не для флоу данных в проекта, а как средство проброса ивентов и эффектов
DmitryKazakov8
20.06.2025 20:13В вашем контексте "разработка навынос", то есть заказная, сроки - сжатые, команда - минимальная и низкой квалификации, статья и аргументы годные для "песочницы". Но Хабру это пользы не несет, поэтому поставил минус за низкое качество материала. После 20 таких проектов - сами увидите, насколько плох был подход и подобранные библиотеки.
VadimLatypov Автор
20.06.2025 20:13А давно хабр стал местом куда могут постить только «полезные» как вам кажется статьи? (Никогда таким и не был, как бы вам этого не хотелось)
Ладно, ваше мнение)
CzarOfScripts
20.06.2025 20:13Nesting и в css есть.
DmitryKazakov8
20.06.2025 20:13Троллинг? Напишите валидный нестинг на css, опишите поддерживаемые браузеры. Никому не нужен такой бесполезный коммент
CzarOfScripts
20.06.2025 20:13https://developer.mozilla.org/en-US/docs/Web/CSS/Nesting_selector ну возьми почитайте и там же внизу есть поддерживаемые браузеры (то есть все).
DmitryKazakov8
20.06.2025 20:13"возьми почитал" мобильный Сафари 17.2. Интересно, на какие страны вы делаете приложения - в Америке валом старых айфонов, в России - тем более, в Азии 17 еще не видели.
В контексте "еще в 2021 css убил scss" ну сами посмотрите
DmitryKazakov8
20.06.2025 20:13React как view устарел) Его подход с ререндером компонентов даже с Mobx вызывает всю цепочку сравнений VDOM. В целом он очень неоптимизирован. React is ecosystem пока остается актуальным.
popuguytheparrot
20.06.2025 20:13Классическую? кто эти плохие практики с мобх, сасс записал в классику? Не нужно свои фантазии выдавать как за идеальный выбор.
carakaton
20.06.2025 20:13не найдя хороших материалов по теме организации
queryKey
в TanStack QueryЕсть статья от создателя Tanstack Query про эффективное описание ключей.
Из нее (и на практике) я подчеркнул, например, что порой нужно инвалидировать списки отдельно от деталей, если данные деталей подставлены в кеш напрямую из ответа мутации.Я использую рекомендованный автором набор ключей, но автоматизировал создание для каждого ресурса:
type QueryKeys<T extends string> = { all: [T]; lists: [T, 'lists']; list: (params: Record<string, unknown>) => [T, 'lists', typeof params]; details: [T, 'details']; detail: (id: string | number) => [T, 'details', typeof id]; }; function generateKeys<T extends string>(base: T): QueryKeys<T> { return { all: [base], lists: [base, 'lists'], list: (params) => [base, 'lists', params], details: [base, 'details'], detail: (id) => [base, 'details', id], }; } const userQueryKeys = generateKeys('user')
Возможно, это будет Вам полезно.
stozen
20.06.2025 20:13Из личного опыта у FSD ужасный длинный нейминг, что не позволяет создавать странички копи пастой, меняя пару хендлеров вызвов апи Tanstak query лучше использовать в виде функции которые нужно вызывать в контролируемом режиме, без всяких enabled. Инвалидация старых данных там боль. Можно написать над этим обертку, но зачем тогда Tanstak. Лучше написать свою. В целом тоже сомнительная вещь.
VadimLatypov Автор
20.06.2025 20:13Ну в моей реализации все пришло к тому что хуки запросов оборачиваются в хук/функцию, и вызываются где надо с прокидыванием внутрь параметров нужных, а энейблед чисто что бы запросы не вызывались с undefined если их кто то просто так хочет вызвать
Fsd по факту загоняет хаос немного в рамки, да есть минусы, они есть везде, просто надо принять с какими готовы мириться, а с какими нет
Vitaly_js
Из текста не очень понял несколько вещей.
Вот для чего нужна вот эта вундервафля?
На вид, все что она делает заменяет это:
На это:
Это ее единственная функция?
Плюс, не очень понял как пользоваться userQueryKeys
Например, у нас есть
userQueryKeys.card
который состоит из'users', id, 'card'
на вид это выглядит так, что совершенно валидно инвалидировать все карточки использовав предикат:Но тогда возникает вопрос, а не удобнее было бы семантически группировать ключи? Что бы все, что относится к card было в одном месте.
И тут возникает третий вопрос, а почему решили не рассматривать подход, который предлагают рассмотреть в доке к https://tanstack.com/query/latest/docs/framework/react/guides/query-keys#further-reading, а именно https://github.com/lukemorales/query-key-factory
VadimLatypov Автор
Да, это просто по факту меняет вид написания, на вид стало лучше, как мне кажется
userQueryKeys, в данном случае централизует, ключи по категории например user
Если рассматривать именно на этом коде
то вызывая
мы инвалидруем только карточку юзера с id 543, а если будет
то всю бизнес сущность юзера, как каждого, так и каждого по id, так и карточку каждого пользователя, а ещё по фильтру
запись коротка и радует глаз, опять же как мне кажется
А вот такой либы https://github.com/lukemorales/query-key-factory, у нас нет в нексусе, из-за особенностей сферы разработки, а базовый подход слишком сильно развязывает руки разработчику и децентрализует ключи, опять же как мне кажется.
Повторюсь это мой первый опыт в таком деле, возможно далее если будет необходимость в сложной инвалидации оно может разростись, но для начала, по читаемости это очень неплохой вриант