Рекурсия в JavaScript

Как насчёт динамического меню когда глубина вложенности неизвестна? Пример из жизни — сайдбар. Под катом реализация для React.

Данные в качестве мока:

// tree.js
const tree = [
  { name: 'Условия размещения' },
  {
    name: 'Кто может размещать объявления',
    children: [
      { name: 'Какие категории существуют?' },
      {
        name: 'Как добавить свое объявление',
        children: [
          {
            name: 'Через личный кабинет',
            children: [
              { name: 'Вот так добавь' }
            ],
          },
          { name: 'Через менеджера' }
        ]
      },
    ]
  },
  {
    name: 'Сколько стоит размещение объявления',
    children: [
      { name: 'Частным лицам' },
      { name: 'Организациям' },
    ]
  }
]

export default tree

React

Создать новый проект.

$ npx create-react-app new-project
$ cd new-project
$ npm run start

Структура проекта:

.
├── src
│   ├── App.js
│   ├── App.css
│   ├── components
│   │   └── RecursiveComponent.js
│   ├── index.js
│   └── tree.js
└── public

App - точка входа. Здесь будет жить рекурсивный компонент. Импортируем файл мока, прогоняем с использованием компонента.

// App.js
import './App.css'
import tree from './tree'
import RecursiveComponent from './components/RecursiveComponent'

function App() {
  return (
    <div className="sidebar">
      <ul className="sidebar-list">
        {tree.map(item => (
          <RecursiveComponent key={`${item.name}`} {...item} />
        ))}
      </ul>
    </div>
  )
}

export default App

Внутри компонента отобразить имена элементов первого уровня. Проверить наличие вложенных уровней и, если они есть, вернуть RecursiveComponent снова, передав ему новый список (рекурсия в действии).

Для того, чтобы раскрывать и закрывать активный список по нажатию на кнопку заданы динамические стили style и childStyle. Состояние хранится в active и меняестя по клику. Для отслеживания вложенности введена переменная depth, увеличивающая своё значение с каждым новым шагом.

// RecursiveComponent.js
import { useState } from 'react'

const RecursiveComponent = ({ name, children, depth = 1 }) => {
  const [active, setActive] = useState(false)
  const style = { marginLeft: 10 + (depth * 5 + 5) }
  const childStyle = { display: depth && active ? 'block' : 'none' }
  const onClick = () => setActive(!active)

  return (
    <li className={`parent parent-${depth}`} style={style}>
      <button className="button" onClick={onClick}>
        <span>{name}</span>
      </button>

      {Array.isArray(children) ? (
        <ul className="child" style={childStyle}>
          {children.map(item => (
            <RecursiveComponent key={item.name} depth={depth + 1} {...item} />
          ))}
        </ul>
      ) : null}
    </li>
  )
}

export default RecursiveComponent

Напоследок простенькая стилизация:

.sidebar {
  max-width: 500px;
  margin: 10px auto;
}

ul {
  list-style: none;
  margin: 0;
  padding-left: 0;
}

li {
  padding: 10px;
  border-left: 1px solid gainsboro;
  text-align: left;
}

.button {
  background-color: #28bd8b;
  border: 1px solid #28bd8b;
  outline: none;
  font-size: 14px;
  color: white;
  font-weight: bold;
  padding: 7px 20px;
  transition: background-color .5s ease;
}

Таков итог.

Recursion