Практика по исправлению рекурсивных импортов во фронтенд приложении
- вторник, 13 января 2026 г. в 00:00:08
Ранее публиковал теоретическую часть по рекурсивным импортам, желательно ознакомиться перед тем как продолжить, чтобы было общее преставление.
Рекурсивные импорты рассмотрим на примере React/Redux приложении.
Исходный код приложения опубликован тут, можете склонировать и попробовать самостоятельно исправить ошибки. Так сказать закрепить теорию на практике.
Частая ошибка, связанная с рекурсивными импортами — это использование barrel файлов. Желательно их не использовать где попало.
Например, в modules не нужно создавать barrel файл index.ts, который реэкспортирует другие модули, т.к. может быть такое, что модули могут в рамках себя переиспользовать другие модули. Но тут есть разные варианты решения этой проблемы: если мы отказываемся от barrel файла в папке modules, мы импортируем тогда конкретный модуль.
Корректно:
import { clientActions } from "@modules/client";Некорректно:
import { clientActions } from "@modules";А если мы в рамках одного модуля экспортируем внутренние модули, то необходимо использовать относительный импорт.
Пример:
// Внутри модуля @modules/client
import { someHelper } from "./helpers";
import { clientTypes } from "./types";Не нужно в barrel файл @modules/client/index.ts добавлять реэкспорт наружу файлов redux.tsx и ui, если они не используются снаружи. В концепции Feature Slices Design это упоминается как high cohesion (высокое зацепление) — участки кода, логически связанные между собой, помещаются рядом.
Некорректно:
// @modules/client/index.ts
export { clientActions, clientReducer } from "./redux";
export { ClientComponent } from "./ui";
export { clientTypes } from "./types";Корректно (если redux и ui используются только внутри модуля):
// @modules/client/index.ts
// Экспортируем только публичный API модуля
export { ClientService } from "./service";
export type { Client } from "./types";
Между модулями: используйте абсолютные импорты через алиасы (@modules/client)
// В модуле @modules/order/service.ts
import { clientActions } from "@modules/client";Внутри модуля: используйте относительные импорты (./, ../)
// В файле @modules/client/service.ts
import { clientActions } from "./redux/actions";
import { clientTypes } from "./types";
import { formatDate } from "../utils/format"; // Импорт из родительской папкиИзбегайте: barrel файлов на верхнем уровне modules/index.ts
Не используйте: импорт barrel файла внутри модуля через '.' (он же './index.ts') или '../' (он же '../index.ts'), используйте прямой относительный импорт. Например, в файле service.ts, если нам нужен redux:
Корректный импорт:
import { clientActions } from "./redux";Некорректный импорт:
import { clientActions } from "."; // Импорт через barrel файлВизуально это выглядит не очень и может создать рекурсивные импорты.
Barrel файлы уместны:
Внутри модуля для экспорта публичного API
Когда нет риска циклических зависимостей
Когда модуль является конечной точкой (leaf node) в графе зависимостей
Leaf node (листовой узел) — это модуль, который сам не импортирует другие модули проекта (или импортирует только внешние библиотеки), но может быть импортирован другими модулями. Такой модуль находится в конце цепочки зависимостей и не может создать циклическую зависимость.
Пример графа зависимостей:

В этом примере:
utils — leaf node: не зависит от других модулей проекта, только от внешних библиотек
client — зависит от utils, но не от других модулей
order — зависит от client и utils
Пример небезопасного barrel файла:
// modules/index.ts - НЕБЕЗОПАСНО!
// Если client и order импортируют друг друга, возникнет цикл
export * from "./client";
export * from "./order";Для обнаружения рекурсивных импортов можно использовать инструменты анализа зависимостей:
[madge](https://github.com/pahen/madge) — визуализация графа зависимостей и обнаружение циклических зависимостей
[dependency-cruiser](https://github.com/sverweij/dependency-cruiser) — анализ и валидация зависимостей
Линтеры — некоторые ESLint плагины могут обнаруживать циклические зависимости
Автодополнение (автоподсказка и применяемые изменения при нажатии на кнопку Tab) и Quick Edit (вызывается правой кнопкой мыши по коду) в Cursor чаще всего предлагают поверхностное исправление. Возможно, где-то это поможет, а где-то оставит структурную проблему, но в моменте исправит.
Автоматические исправления могут предложить следующие решения:
1. Добавление type к импорту типов
Даже если между модулями есть рекурсивные импорты, в финальном бандле этот импорт удалится, т.к. type относится к TypeScript (данное решение маскирует проблему)
Пример: import type { User } from './types' вместо import { User } from './types'
Использование type в импортах может временно решить проблему, но маскирует реальную проблему организации кода.
2. Исправление импорта с barrel файла на конкретный модуль
Решает проблему, но не всегда корректно, т.к. может импортировать модуль напрямую
Некорректный пример (предложенный Cursor):
import { clientActions } from "@modules/client/redux";Корректный (вот тут, например, возникает рекурсивный импорт):
import { clientActions } from "@modules/client";3. Рефакторинг структуры кода (рекомендуемый подход)
Внести правки в организацию кода: вынести в отдельный модуль общие части из обоих модулей, либо разделить на более мелкие модули, чтобы уменьшить зацепление и таким образом разорвать рекурсивные импорты.
Пример рефакторинга:
До (проблема с рекурсивными импортами):
// @modules/client/service.ts
import { orderService } from "@modules/order";
// @modules/order/service.ts
import { clientService } from "@modules/client";
// Циклическая зависимость: client → order → clientПосле (решение через выделение общего модуля):
// @modules/shared/types.ts - общие типы
export type { Client, Order } from "./types";
// @modules/client/service.ts
import type { Client, Order } from "@modules/shared/types";
import { orderService } from "@modules/order";
// @modules/order/service.ts
import type { Client, Order } from "@modules/shared/types";
import { clientService } from "@modules/client";
// Теперь оба модуля зависят от shared, но не друг от друга напрямуюАльтернативное решение (разделение на более мелкие модули):
// Разделяем client на client-core и client-ui
// @modules/client-core/service.ts - базовая логика
// @modules/client-ui/component.tsx - UI компоненты
// @modules/order/service.ts теперь импортирует только client-core
import { clientCoreService } from "@modules/client-core";Если перед вами стоит задача разрешить рекурсивные импорты, это значит, что до этого в вашем проекте отсутствовали четкие правила разделения кода, либо было пренебрежение к ним.
Важно: Для того чтобы начать исправлять ошибку, необходимо пройтись по всей цепочке импортов, т.к. быстрое исправление в одном месте только маскирует проблему плохо организованного кода.
Прежде чем приступать к исправлению, необходимо:
Обнаружение рекурсивные импортов в проекте с помощью плагина eslint-plugin-import
Определить причины возникновения циклических зависимостей
Выявить архитектурные проблемы: неправильное использование barrel файлов, нарушение принципов модульности, отсутствие четких границ между модулями
Оценить масштаб проблемы: сколько модулей затронуто, какие из них критичны
После анализа необходимо:
Выстроить план по постепенному исправлению ошибок с приоритизацией:
Начать с наиболее критичных модулей (часто используемых, влияющих на другие части системы)
Исправить простые случаи (например, неправильное использование barrel файлов)
Затем перейти к сложным рефакторингам (выделение общих модулей, разделение на более мелкие части)
Определить целевые правила организации кода (например, следование Feature Slices Design)
Документировать правила импортов и структуры модулей для команды
Чтобы предотвратить появление новых проблем:
Написание нового кода с учетом разработанного плана и правил
Обучение команды новым правилам и принципам организации кода
Проведение код-ревью с учетом этого плана, т.к. необходимо изменить привычку писать "по-привычному"
Документирование примеров правильного и неправильного использования импортов
Настроить автоматические проверки, чтобы разработчики видели нарушения правил в IDE и не могли залить проблемный код:
Настроить правила ESLint на обнаружение рекурсивных зависимостей
Использовать eslint-plugin-boundaries для запрета определенных импортов между слоями/модулями
Настроить автоматический запуск ESLint при pull request, чтобы нельзя было залить изменения, если присутствуют рекурсивные импорты
Интегрировать проверки в CI/CD для автоматической валидации зависимостей
Рекурсивные импорты — это симптом плохой организации кода, а не просто техническая проблема. Они возникают, когда отсутствуют четкие правила разделения кода или когда разработчики пренебрегают архитектурными принципами.
1. Не маскируйте проблему: Использование type импортов или поверхностные исправления в одном месте только скрывают проблему, но не решают её. Необходимо анализировать всю цепочку зависимостей.
2. Правильная организация с самого начала: Следование принципам Feature Slices Design или другим архитектурным подходам, правильное использование barrel файлов и четкое разделение модулей поможет избежать этих проблем на этапе проектирования.
3. Barrel файлы — инструмент, а не решение всех проблем: Используйте их осознанно — только внутри модулей для экспорта публичного API и только когда модуль является leaf node в графе зависимостей.
4. Автоматизация — ваш друг: Настройте ESLint правила и CI/CD проверки, чтобы предотвратить появление новых рекурсивных импортов и помочь команде следовать установленным правилам.
5. Рефакторинг требует системного подхода: При исправлении существующих проблем необходим анализ, планирование, обучение команды и постепенное внедрение изменений.
Помните: хорошо организованная архитектура модулей не только предотвращает рекурсивные импорты, но и делает код более поддерживаемым, тестируемым и понятным для всей команды.
Этот проект можно использовать как практическое задание для работы с AI-ассистентами, такими как Cursor. Вот пошаговый подход:
Попросите Cursor проанализировать статью и проект, чтобы он:
Сформировал файл markdown с предполагаемыми ошибками в проекте
Предложил решения для каждой ошибки
Составил поэтапный план исправления
После того как Cursor предложит свои выводы:
Внимательно ознакомьтесь с предложенным планом
Подкорректируйте его выводы, если необходимо
Убедитесь, что план учитывает все нюансы вашего проекта
Дайте Cursor задание исправить обнаруженные ошибки, но делайте это постепенно:
Исправляйте ошибки по одной или небольшими группами
Проверяйте, что решения не ломают функциональность
Тестируйте после каждого изменения
Такой подход поможет безопасно рефакторить код и избежать проблем в production.