Рекурсивные компоненты

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

Типичные примеры задач:

  • дерево файлов и папок;
  • комментарии с неограниченной вложенностью;
  • меню с подменю;
  • категории и подкатегории;
  • JSON-структуры произвольной глубины.

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


Принцип работы рекурсивного рендера

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

  1. Компонент должен иметь имя (name)
  2. Должно существовать условие остановки рекурсии

Компонент вызывает сам себя только при выполнении определённого условия. Если условие отсутствует, Vue попытается рендерить компонент бесконечно, что приведёт к переполнению стека.


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

Простейший пример компонента, который отображает вложенный список элементов:

<template>
  <ul>
    <li v-for="item in items" :key="item.id">
      {{ item.title }}
      <RecursiveList
        v-if="item.children && item.children.length"
        :items="item.children"
      />
    </li>
  </ul>
</template>

<script>
export default {
  name: 'RecursiveList',
  props: {
    items: {
      type: Array,
      required: true
    }
  }
}
</script>

Ключевые моменты:

  • name: 'RecursiveList' позволяет компоненту ссылаться на себя
  • рекурсия происходит только при наличии children
  • глубина вложенности определяется структурой данных

Роль имени компонента

Во Vue рекурсивный вызов возможен только по имени компонента, а не по переменной импорта.

Некорректно:

components: { RecursiveList }

и затем использование <RecursiveList /> без name.

Корректно:

export default {
  name: 'RecursiveList'
}

Vue использует name как идентификатор при компиляции шаблона. Без него рекурсия не будет работать.


Условие остановки рекурсии

Условие остановки — критически важная часть рекурсивного компонента. Оно определяет момент, когда вложенный вызов прекращается.

На практике используются следующие варианты:

  • отсутствие дочерних элементов;
  • достижение определённого уровня глубины;
  • флаг в данных (isLeaf, expanded);
  • проверка типа узла.

Пример с ограничением глубины:

<RecursiveNode
  v-if="level < maxLevel"
  :node="child"
  :level="level + 1"
/>

Передача данных между уровнями рекурсии

Каждый уровень рекурсии — это отдельный экземпляр компонента со своим состоянием и пропсами.

Обычно передаются:

  • текущий узел данных;
  • уровень вложенности;
  • настройки отображения;
  • callback-функции.

Пример структуры данных:

const tree = [
  {
    id: 1,
    title: 'Корень',
    children: [
      {
        id: 2,
        title: 'Дочерний узел',
        children: []
      }
    ]
  }
]

Компонент принимает один узел и отображает его рекурсивно:

<template>
  <div class="node">
    {{ node.title }}
    <div class="children" v-if="node.children?.length">
      <TreeNode
        v-for="child in node.children"
        :key="child.id"
        :node="child"
      />
    </div>
  </div>
</template>

<script>
export default {
  name: 'TreeNode',
  props: {
    node: Object
  }
}
</script>

Рекурсивные компоненты и Composition API

В Vue 3 рекурсия одинаково работает как с Options API, так и с Composition API.

Пример с <script setup>:

<script setup>
defineProps({
  node: Object
})
</script>

<template>
  <div>
    {{ node.title }}
    <TreeNode
      v-for="child in node.children || []"
      :key="child.id"
      :node="child"
    />
  </div>
</template>

Имя компонента в этом случае определяется именем файла (TreeNode.vue). Это автоматически делает рекурсию возможной без явного name.


Управление состоянием вложенных элементов

Частая задача — управление состоянием каждого уровня (раскрыт/свернут, выбран, активен).

Состояние может храниться:

  • внутри каждого узла данных;
  • локально в компоненте;
  • во внешнем хранилище (Pinia, Vuex).

Пример локального состояния:

<script>
export default {
  name: 'TreeNode',
  props: {
    node: Object
  },
  data() {
    return {
      open: false
    }
  }
}
</script>
<div @click="open = !open">
  {{ node.title }}
</div>

<TreeNode
  v-if="open"
  v-for="child in node.children"
  :key="child.id"
  :node="child"
/>

Каждый уровень управляет собственным состоянием независимо.


События в рекурсивных компонентах

Рекурсивные компоненты часто должны уведомлять родительские уровни о событиях:

  • выбор элемента;
  • удаление;
  • изменение данных.

Используется проброс событий вверх:

<TreeNode
  @select="emit('select', $event)"
/>
emits: ['select']

Таким образом событие поднимается через всю цепочку до корневого компонента.


Производительность и ограничения

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

  • большие деревья создают большое количество компонентов;
  • каждый уровень — отдельный watcher;
  • глубокая вложенность увеличивает нагрузку на DOM.

Рекомендации:

  • использовать key обязательно;
  • избегать лишних реактивных данных;
  • применять v-show вместо v-if, если структура статична;
  • рассматривать виртуализацию для больших деревьев.

Распространённые ошибки

Отсутствие условия остановки Приводит к бесконечному рендеру и ошибке Maximum call stack size exceeded.

Отсутствие имени компонента Рекурсия не работает без name.

Мутация пропсов Изменение данных узла напрямую нарушает однонаправленный поток данных.

Глобальная регистрация без необходимости Рекурсивные компоненты чаще эффективнее как локальные.


Архитектурные преимущества

Рекурсивные компоненты:

  • идеально отражают структуру данных;
  • упрощают поддержку вложенных интерфейсов;
  • уменьшают дублирование кода;
  • легко масштабируются по глубине.

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