javascript

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

  • вторник, 13 января 2026 г. в 00:00:08
https://habr.com/ru/articles/984382/

Ранее публиковал теоретическую часть по рекурсивным импортам, желательно ознакомиться перед тем как продолжить, чтобы было общее преставление.

Рекурсивные импорты рассмотрим на примере React/Redux приложении.

Исходный код приложения опубликован тут, можете склонировать и попробовать самостоятельно исправить ошибки. Так сказать закрепить теорию на практике.

Barrel файлы и рекурсивные импорты

Частая ошибка, связанная с рекурсивными импортами — это использование barrel файлов. Желательно их не использовать где попало.

Проблема с barrel файлами в папке modules

Например, в 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";

Принцип высокого зацепления (High Cohesion)

Не нужно в 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";

Рекомендации и best practices

1. Структура модулей

структура проекта
структура проекта

2. Правила импортов

  • Между модулями: используйте абсолютные импорты через алиасы (@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 файл

Визуально это выглядит не очень и может создать рекурсивные импорты.

3. Когда использовать barrel файлы

Barrel файлы уместны:

  • Внутри модуля для экспорта публичного API

  • Когда нет риска циклических зависимостей

  • Когда модуль является конечной точкой (leaf node) в графе зависимостей

Что такое leaf node (конечная точка) в графе зависимостей?

Leaf node (листовой узел) — это модуль, который сам не импортирует другие модули проекта (или импортирует только внешние библиотеки), но может быть импортирован другими модулями. Такой модуль находится в конце цепочки зависимостей и не может создать циклическую зависимость.

Пример графа зависимостей:

граф зависимостей
граф зависимостей

В этом примере:

  • utilsleaf node: не зависит от других модулей проекта, только от внешних библиотек

  • client — зависит от utils, но не от других модулей

  • order — зависит от client и utils

Пример небезопасного barrel файла:

// modules/index.ts - НЕБЕЗОПАСНО!
// Если client и order импортируют друг друга, возникнет цикл
export * from "./client";
export * from "./order";

4. Обнаружение проблем

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

  • [madge](https://github.com/pahen/madge) — визуализация графа зависимостей и обнаружение циклических зависимостей

  • [dependency-cruiser](https://github.com/sverweij/dependency-cruiser) — анализ и валидация зависимостей

  • Линтеры — некоторые ESLint плагины могут обнаруживать циклические зависимости

Проблема автоматических исправлений

Автодополнение (автоподсказка и применяемые изменения при нажатии на кнопку Tab) и Quick Edit (вызывается правой кнопкой мыши по коду) в Cursor чаще всего предлагают поверхностное исправление. Возможно, где-то это поможет, а где-то оставит структурную проблему, но в моменте исправит.

Как 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";

С чего начать

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

Важно: Для того чтобы начать исправлять ошибку, необходимо пройтись по всей цепочке импортов, т.к. быстрое исправление в одном месте только маскирует проблему плохо организованного кода.

1. Анализ текущего состояния

Прежде чем приступать к исправлению, необходимо:

  • Обнаружение рекурсивные импортов в проекте с помощью плагина eslint-plugin-import

  • Определить причины возникновения циклических зависимостей

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

  • Оценить масштаб проблемы: сколько модулей затронуто, какие из них критичны

2. Разработка плана исправления

После анализа необходимо:

  • Выстроить план по постепенному исправлению ошибок с приоритизацией:

  • Начать с наиболее критичных модулей (часто используемых, влияющих на другие части системы)

  • Исправить простые случаи (например, неправильное использование barrel файлов)

  • Затем перейти к сложным рефакторингам (выделение общих модулей, разделение на более мелкие части)

  • Определить целевые правила организации кода (например, следование Feature Slices Design)

  • Документировать правила импортов и структуры модулей для команды

3. Внедрение правил в процесс разработки

Чтобы предотвратить появление новых проблем:

  • Написание нового кода с учетом разработанного плана и правил

  • Обучение команды новым правилам и принципам организации кода

  • Проведение код-ревью с учетом этого плана, т.к. необходимо изменить привычку писать "по-привычному"

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

4. Автоматизация проверок

Настроить автоматические проверки, чтобы разработчики видели нарушения правил в 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. Рефакторинг требует системного подхода: При исправлении существующих проблем необходим анализ, планирование, обучение команды и постепенное внедрение изменений.

Помните: хорошо организованная архитектура модулей не только предотвращает рекурсивные импорты, но и делает код более поддерживаемым, тестируемым и понятным для всей команды.

Бонус. Работа с Cursor

Этот проект можно использовать как практическое задание для работы с AI-ассистентами, такими как Cursor. Вот пошаговый подход:

Шаг 1: Анализ проекта

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

  • Сформировал файл markdown с предполагаемыми ошибками в проекте

  • Предложил решения для каждой ошибки

  • Составил поэтапный план исправления

Шаг 2: Проверка и корректировка

После того как Cursor предложит свои выводы:

  • Внимательно ознакомьтесь с предложенным планом

  • Подкорректируйте его выводы, если необходимо

  • Убедитесь, что план учитывает все нюансы вашего проекта

Шаг 3: Постепенное исправление

Дайте Cursor задание исправить обнаруженные ошибки, но делайте это постепенно:

  • Исправляйте ошибки по одной или небольшими группами

  • Проверяйте, что решения не ломают функциональность

  • Тестируйте после каждого изменения

Такой подход поможет безопасно рефакторить код и избежать проблем в production.