Эта статья — про то, как спокойно и осознанно писать код интерфейса. Не быстро, не «в лоб», а так, чтобы через полгода в него было не страшно вернуться.

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

Почему вообще стоит говорить о проектировании

Большинство проблем в UI-коде появляются не из-за JavaScript, а из-за того, что:

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

В маленьких примерах это не заметно.

В реальных проектах — очень больно.

Реальная задача

Мы делаем уведомление:

  • показывает текст
  • закрывается по кнопке
  • может закрываться из кода
  • не ломает остальной интерфейс

Это простой компонент, но достаточно показательный.

Как обычно пишут такой код

Почти всегда начинают с DOM:

const el = document.querySelector('.notification');
el.querySelector('.close').addEventListener('click', () => {
  el.style.display = 'none';
});

Почему это кажется нормальным:

  • код короткий
  • результат виден сразу

Почему это плохо:

  • уведомление жёстко привязано к HTML
  • нельзя создать второе уведомление
  • невозможно управлять из JS
  • состояние нигде не описано

Это не компонент, а кусок скрипта.

Первый важный шаг — подумать о состоянии

Перед кодом всегда стоит задать вопрос:

что здесь является состоянием?

В нашем случае всё просто:

  • уведомление либо видно
  • либо закрыто

Это состояние:

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

Если состояние открыто — его обязательно кто-нибудь сломает.

Очень важная мысль: DOM — не состояние

Частая ошибка — считать, что:

element.style.display = 'none';

и есть состояние.

Это не так.

DOM — это только отражение состояния.

Если использовать его как источник истины:

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

Правильный подход — сначала состояние, потом DOM.

Начинаем с фабрики компонента

Компонент создаётся функцией.

function createNotification(message) {
  let isVisible = true;

Что здесь происходит:

  • isVisible — внутреннее состояние
  • оно приватное
  • живёт столько же, сколько компонент

Никакой глобальной области, никакого window.

Создаём DOM-структуру

Теперь создаём элементы.

  const element = document.createElement('div');
  element.className = 'notification';

  const text = document.createElement('span');
  text.textContent = message;

  const closeButton = document.createElement('button');
  closeButton.textContent = '×';

  element.appendChild(text);
  element.appendChild(closeButton);

Важно:

  • мы не вставляем элемент в DOM
  • компонент ещё не «живёт»
  • нет побочных эффектов

Это делает код предсказуемым и тестируемым.

Обновление интерфейса — отдельная логика

Теперь ключевая часть — render.

  function render() {
    element.style.display = isVisible ? 'block' : 'none';
  }

Это очень простой код, но он задаёт правильное мышление:

интерфейс всегда следует за состоянием

Мы не прячем элемент в обработчике клика.

Мы меняем состояние и перерисовываем интерфейс.

Управление состоянием

  function close() {
    if (!isVisible) return;

    isVisible = false;
    render();
  }

Почему так лучше:

  • состояние меняется в одном месте
  • невозможно закрыть уведомление дважды
  • поведение легко расширить

Например, добавить анимацию, таймер или колбэк.

События — внутренняя деталь компонента

  closeButton.addEventListener('click', close);

Обрати внимание:

  • внешний код не знает про кнопку
  • не знает про DOM-структуру
  • не знает про логику закрытия

Это инкапсуляция в чистом виде.

Инициализация компонента

  render();

Компонент сам приводит себя в корректное состояние.

Внешний код не обязан помнить, что нужно что-то вызвать.

Возвращаем API, а не реализацию

  return {
    element,
    close
  };
}

Это одна из самых важных строк статьи.

Снаружи доступно только:

  • element — чтобы вставить компонент
  • close() — бизнес-действие

Нельзя:

  • изменить isVisible
  • тронуть внутренние элементы
  • сломать логику обновления

Это делает компонент надёжным.

Использование компонента

const notification = createNotification('Saved!');
document.body.appendChild(notification.element);

Или программное управление:

setTimeout(() => {
  notification.close();
}, 3000);

Обрати внимание: внешний код не работает с DOM напрямую.

Почему это правильная практика

Даже в этом маленьком примере мы видим:

  • чёткую ответственность
  • изолированное состояние
  • понятный жизненный цикл
  • простой и честный API

Если завтра потребуется:

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

код не придётся выбрасывать.

Почему так делают фреймворки

React, Vue и другие решают те же проблемы:

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

Фреймворки автоматизируют это.

Но если не понимать основу, они превращаются в магию.

Главный вывод

Хороший UI-компонент:

  • начинается с понимания состояния
  • не использует DOM как источник истины
  • имеет чёткие границы
  • сложно использовать неправильно

Если этот подход усвоен, дальше уже не важно — пишешь ли ты на чистом JS или с фреймворком.