Я попробовал Solid.js — и начинаю ненавидеть React
- пятница, 17 октября 2025 г. в 00:00:04
Команда JavaScript for Devs подготовила перевод статьи, в которой разработчик с восьмилетним опытом работы с React делится неожиданным открытием: Solid.js оказался проще, логичнее и… приятнее в использовании. Меньше перерендеров, ближе к нативному вебу, честное поведение API и настоящие веб-компоненты — кажется, у React появился достойный конкурент.
Прежде чем перейти к сути статьи, немного предыстории: я работаю с React почти восемь лет и любил каждую секунду. На нём я делал Open source, приложения и всё, что между этими крайностями.
Недавно я присоединился к команде TanStack, чтобы работать над TanStack Devtools, и у них Solid.js принят в качестве базы для всех проектов с UI. Я решил собраться с духом и выучить Solid.js. В технических выборах я стараюсь быть непредвзятым, так что подошёл к делу с открытым взглядом и желанием учиться новому!
Я месяц работаю с Solid и всё ещё новичок, но должен признать: самым трудным для понимания оказалась именно реактивность. В мире React вы привыкли к такой модели:
вы создаёте компонент со стейтом и пропсами
стейт и пропсы меняются
компонент перерендеривается
Всё довольно просто! При изменении стейта или пропсов компонент ререндерится. Но в Solid этот подход перевёрнут с ног на голову: перерендеры происходят только тогда, когда вы явно этого хотите — через встроенную реактивность (например, сигналы). То есть если вы хотите, чтобы ваш компонент перерендерился из-за изменения пропса, вам нужно явно прописать «слушатель» — через createMemo
или createStore
. И только тогда компонент будет перерендерён. В отличие от React, у Solid философия такая: «Я отрендерился и больше этого делать не буду, как бы ни менялись данные, пока ты мне прямо не скажешь».
Звучит ужасно для человека из мира React: если у вас куча пропсов, которые меняются, пришлось бы обернуть их все в какой-нибудь «хук», чтобы сделать их реактивными, верно?
На самом деле нет. Пропсы по умолчанию не реактивные, но становятся таковыми, если это сигналы. То есть если вы создаёте состояние в родителе на базе сигнала и передаёте его в дочерний компонент, такой проп реактивен. На практике вы получаете почти идентичный React API, только без лишних перерендеров.
Поработав с этим и помучившись, пытаясь заставить что-то перерендериться, я понял важную вещь об архитектуре Solid.js, которая даёт серьёзное преимущество перед React:
Гораздо проще добиться перерендеринга тогда, когда он вам нужен, чем добиваться того, чтобы он не происходил, когда он не нужен!!
Если вы ещё не знали: помимо TanStack я плотно вовлечён в экосистему Remix/React Router, и один из важнейших принципов, который я оттуда вынес, — «используйте платформу». По сути это сводится к: «Если есть веб-API, пользуйтесь им, а не изобретайте свои велосипеды и абстракции средствами конкретного фреймворка». Так вот, Solid.js ощущается куда ближе к платформе — объясню на примерах.
Вы когда-нибудь работали с иконками в проекте? Приходилось идти в что-то вроде lucide-react, находить подходящую иконку, копировать её и вставлять в код React — а если у вас не было какого-нибудь изощрённого пайплайна, который превращает иконки в спрайт-листы или что-то подобное, то, скорее всего, приходилось проходиться по каждому SVG и менять атрибут class
на className
!
Так вот: в Solid нет особых ключевых слов для JSX. Пропсы HTML-элементов необязательно писать в camelCase
— можно, но не нужно. Нет «запрещённых» атрибутов вроде class
или for
, что держит вас ближе к самой платформе. Диалект JSX у Solid ощущается куда приятнее, чем у React, а новичкам он помогает учиться работать с платформой по настоящей спецификации HTML, а не по «придуманной» React — и это мне очень по душе!
Пока я работал над @tanstack
/devtools
и адаптерами для React и Solid, заметил то, чего нет в React: в Solid API ведут себя именно так, как вы и ждёте — и даже больше! Объясню на примере порталов.
Чтобы добавить в devtools «отсоединённый» режим (когда их можно вынести в отдельное окно), мне пришлось использовать порталы, чтобы примонтировать devtools в это отдельное окно. Для этого у нас есть ядро на Solid.js, которое монтируется в приложение на React и при этом должно сохранять ваш контекст и состояние, даже когда его монтируют в отдельном окне. Единственный способ сделать это — воспользоваться createPortal
из react-dom
. Ниже покажу точный код, который для этого нужен:
export const TanStackDevtools = ({
plugins,
config,
eventBusConfig,
}: TanStackDevtoolsReactInit): ReactElement | null => {
const devToolRef = useRef<HTMLDivElement>(null)
const [pluginContainers, setPluginContainers] = useState<
Record<string, HTMLElement>
>({})
const [titleContainers, setTitleContainers] = useState<
Record<string, HTMLElement>
>({})
const [PluginComponents, setPluginComponents] = useState<
Record<string, JSX.Element>
>({})
const [TitleComponents, setTitleComponents] = useState<
Record<string, JSX.Element>
>({})
const [devtools] = useState(
() =>
new TanStackDevtoolsCore({
config,
eventBusConfig,
plugins: plugins?.map((plugin) => {
return {
...plugin,
render: (e, theme) => {
const target = e.ownerDocument.getElementById(
e.getAttribute('id')!,
)
if (target) {
setPluginContainers((prev) => ({
...prev,
[e.getAttribute('id') as string]: e,
}))
}
convertRender(plugin.render, setPluginComponents, e, theme)
},
}
}),
}),
)
useEffect(() => {
if (devToolRef.current) {
devtools.mount(devToolRef.current)
}
return () => devtools.unmount()
}, [devtools])
return (
<>
<div style={{ position: 'absolute' }} ref={devToolRef} />
{Object.values(pluginContainers).length > 0 &&
Object.values(PluginComponents).length > 0
? Object.entries(pluginContainers).map(([key, pluginContainer]) =>
createPortal(<>{PluginComponents[key]}</>, pluginContainer),
)
: null}
{Object.values(titleContainers).length > 0 &&
Object.values(TitleComponents).length > 0
? Object.entries(titleContainers).map(([key, titleContainer]) =>
createPortal(<>{TitleComponents[key]}</>, titleContainer),
)
: null}
</>
)
}
Вот как это работает:
devtools монтируются с плагинами, предоставленными пользователем
при клике на плагин он добавляется в массив плагинов
для каждого плагина создаётся реализация createPortal, чтобы сохранить контексты под элементом, к которому примонтированы devtools
Почему это вообще нужно? Хотя createPortal — это функция, которая монтирует JSX в указанный элемент, её недостаток в том, что она ДОЛЖНА быть отрендерена в JSX, иначе не работает. То есть вместо того, чтобы сделать что-то вроде:
render: (e, theme) => {
const target = e.ownerDocument.getElementById(
e.getAttribute('id')!,
)
if (target) {
createPortal(<div>hello</div>, target)
}
},
Нам приходится использовать приведённый выше «монструозный» хак: класть это в состояние, подменять туда-сюда и рендерить через JSX. На этом этапе вы, вероятно, задаётесь вопросом, как это выглядит в Solid.js? Вот, пожалуйста:
export default function SolidDevtoolsCore({
config,
plugins,
eventBusConfig,
}: TanStackDevtoolsInit) {
const [devtools] = createSignal(
new TanStackDevtoolsCore({
config,
eventBusConfig,
plugins: plugins?.map((plugin) => ({
...plugin,
render: (el: HTMLDivElement, theme: 'dark' | 'light') =>
<Portal mount={el}>
{typeof plugin.render === 'function' ? plugin.render(el, theme) : plugin.render}
</Portal>
})),
}),
)
let devToolRef: HTMLDivElement | undefined
createEffect(() => {
devtools().setConfig({ config })
})
onMount(() => {
if (devToolRef) {
devtools().mount(devToolRef)
onCleanup(() => {
devtools().unmount()
})
}
})
return <div style={{ position: 'absolute' }} ref={devToolRef} />
}
Недостаточно предыдущего примера? Поговорим о веб-компонентах. Мы пытались интегрировать веб-компоненты в @tanstack
/devtools-ui
, чтобы любой мог использовать их в любом фреймворке практически без настройки. Это значительно упростила библиотека solid-element
из Solid.js, которая превращает любой компонент Solid в веб-компонент — само по себе здорово. Я создал и протестировал компоненты в Solid (это заняло около 30 минут), и пришло время попробовать их в React.
И тут… они вообще не работают в любых версиях младше React 19, потому что React нормально не поддерживает веб-компоненты. А в React 19 любой проп, который вы передаёте веб-компоненту, преобразуется в строку — вам приходится вручную для каждого пропса указывать, что это не строка, чтобы React корректно его передал. А если вы пробуете до React 19 — удачи!
В Solid компоненты просто работают: даже если вы не описали пропсы вручную, всё ведёт себя ожидаемо без дополнительной настройки. Веб-компоненты важны не для всех — я это понимаю. Но, как видно из этого и предыдущего примера, что бы мы ни пробовали сделать, React обычно сопротивляется, а Solid — нет, и просто позволяет это сделать.
Чем больше я работаю с Solid, тем яснее понимаю: мне приходится «бороться» с React по множеству вопросов, по которым не должно быть борьбы. Я не хочу делать громких заявлений, но Solid стал глотком свежего воздуха и показал, каким мог бы быть React — но, вероятно, уже не станет. Экосистема по-прежнему на стороне React, и чтобы это изменить, пришлось бы сдвинуть горы, особенно сейчас, в эпоху ИИ. Лично мне приятнее делать проекты на Solid, чем на React, и если вы решитесь попробовать Solid, думаю, он понравится и вам. У него отличный дизайн, и работать с ним очень приятно, когда понимаешь, как он устроен.
Я вовсе не считаю, что выбирать React — это неправильно, но после знакомства с Solid я точно могу рекомендовать расширить горизонт. С интересом жду, как Solid будет развиваться в ближайшие годы, и очень рад, что познакомился с ним.
Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!