Я переписал свою реактивную библиотеку
Осенью 2026-го я рассказывал здесь про RWC — библиотеку реактивных веб-компонентов на сигналах. Без JSX и шаблонного компилятора: разметка собирается типизированными фабриками div, button, input, а обновляется через сигналы, как в Solid.
Статью прочитали восемь тысяч человек, тридцать оставили комментарии. Я собирался поправить пару шероховатостей, а в итоге переписал ядро.
Ниже — места, которые изменились. И ответ тем, кто спрашивал, зачем всё это, когда есть Lit. Первую статью читать не обязательно.
Что из этого видно снаружи
Декораторы @component, @property, @event убраны — 363 строки магии, требовавшей experimentalDecorators и не дававшей нормального вывода типов. Вместо них одно объявление:
class Counter extends defineComponent({ props: { value: 0, step: 1 }, events: { change: payload<number>() }, }) { render() { const { value, step } = this.props; return div( button({ "@click": () => value.update((v) => v - step()) }, "−"), span(value), button({ "@click": () => value.update((v) => v + step()) }, "+"), ); } } export const counter = useCustomComponent(Counter, "x-counter");
В counter({ "@change": (e) => e.detail }) тип detail — number, потому что так написано в объявлении. Опечатка в имени пропса — ошибка компиляции, а не проигнорированный атрибут.
Функция теперь реактивный источник везде, где значение не может быть функцией: span(() => count() * 2) работает без computed. null, undefined и false среди детей пропускаются, так что isAdmin() && button("удалить") делает ожидаемое. Появился resource — data, loading, error, refetch, отмена предыдущего запроса через AbortSignal. А одинаковый CSS даёт один CSSStyleSheet на страницу через adoptedStyleSheets: тысяча кнопок — одна таблица стилей, а не тысяча тегов <style>.
Теперь про комментарии к прошлой статье
Самый частый вопрос был прямой: зачем, если есть Lit, Stencil, Solid?
Честный ответ: если вам подходит Lit — берите Lit. Он зрелее, у него больше пользователей и за ним стоит Google. Отличий два. Первое — разметка без шаблонных строк: у Lit это html с интерполяцией, то есть текст, проверяемый в рантайме; здесь разметка — обычный TypeScript, и переход к определению, переименование и поиск ссылок работают штатными средствами редактора, без плагина. Второе — @state в Lit перерисовывает весь шаблон, а сигнал обновляет конкретный текстовый узел.
Второй упрёк — про поддержку: стоит выбрать более зрелое решение. Возразить нечего, это правда. Могу только показать, что изменилось: тесты, документация на двух языках, проверка собранного пакета глазами потребителя, а версии 2.x остались под тегом и веткой legacy/v2, а не исчезли. С помощью ИИ агентов планирую быстро собрать на этом ядре всю необходимую экосистему.
Третий — самый интересный, потому что по большей части справедливый. Тезис: сами Web Components плохи — хостовые объекты медленные, атрибуты только строковые, имена тегов глобальны, теневой DOM мешает стилям.
Про строки в атрибутах — правда, и это не лечится: значение разбирается JSON.parse, а что не разобралось, остаётся строкой. Поэтому основной путь передачи данных здесь — DOM-свойства (.value), а атрибуты нужны для разметки, написанной руками.
Про глобальные имена — тоже правда, и вот тут удалось сделать хоть что-то. Обычно тег прибит к компоненту намертво: customElements.define("uwc-button", Button) где-то в недрах кита. Второй такой вызов на странице — ошибка.
configCustomComponent разделяет объявление и регистрацию. Кит только объявляет и отдаёт наружу пару «фабрика + регистратор»:
// @company/ui-kit — здесь customElements.define не вызывается class Button extends defineComponent({ props: { type: "secondary" } }) { render() { return button({ class: `btn btn-${this.props.type()}` }, slot()); } } export const [UwcButton, registerUwcButton] = configCustomComponent(Button, "uwc-button");
Имя тега выбирает уже приложение — на старте, один раз:
// оболочка, сидит на ките 1.x registerUwcButton({ postfix: "shell" }); // <uwc-button-shell> // виджет на той же странице, приехал со своей сборкой кита 2.x registerUwcButton({ postfix: "widget" }); // <uwc-button-widget>
А теперь главное. Прикладной код в обоих местах — одинаковый, имя тега в нём не упоминается вообще:
UwcButton({ ".type": "primary" }, "Купить");
В DOM при этом оказываются два разных элемента, каждый со своей версией кита внутри:
<uwc-button-shell>Купить</uwc-button-shell> <uwc-button-widget>Купить</uwc-button-widget>
Ни переименований в разметке, ни флагов, ни сборочной магии — разошлась только одна строка на старте приложения. Оговорка: две версии на странице — это две разные сборки кита, то есть два разных класса. Один класс живёт под одним тегом, и попытка зарегистрировать его вторым именем выведет ошибку в консоль, а не создаст клон.
Ради этого сценария библиотека и писалась: как фундамент для кита, который живёт в нескольких приложениях с разным темпом обновления.
Про стили и теневой DOM спорить не буду: это компромисс, и кому нужна сквозная тема без CSS-переменных — будет больно.
А вот с высказываниями на тему «Web Components никому не нужны» не соглашусь. Reddit в 2023-м переехал с React на Lit, Edge собран на веб-компонентах, дизайн-системы Adobe и SAP — тоже. Это не «будущее фронтенда», а рабочий инструмент для одной задачи: компонентов, которые должны пережить смену фреймворка в приложении.
Чего это стоило
Прямого пути обновления с 2.x нет — публичный API другой. Убраны декораторы, роутер (не место ему в такой библиотеке), signal.pipe, forceSet, setName, getSubscribers; forkJoin и combineLatest заменены одним combine. Сигнал несёт ровно три метода: set, peek, update.
При этом ядро уменьшилось: ~3100 строк против 2250 при большем числе возможностей.
Итого
Библиотека: github.com/tamazyanarsen/reactive-web-components, ставится как npm i @rwcjs/core. Внутри — гайд, справочник API и рецепты на русском и английском, плюс папка examples/ с работающими компонентами.
djonnyx
Нет демо. Подобные решения хочется пощупать руками... В описании ни строчки о том, для чего всё это и какие проблемы и кейсы решает. Такое ощущение, что делалось для изучения технологии.
tamazyanarsen Автор
вот тут не понял. Возьмем, например, Lit (ближайший аналог для создания реактивных веб-компонентов). Что там можно пощупать руками?
это один из основных плюсов такого подхода