javascript

Я попробовал Solid.js — и начинаю ненавидеть React

  • пятница, 17 октября 2025 г. в 00:00:04
https://habr.com/ru/articles/955800/

Команда JavaScript for Devs подготовила перевод статьи, в которой разработчик с восьмилетним опытом работы с React делится неожиданным открытием: Solid.js оказался проще, логичнее и… приятнее в использовании. Меньше перерендеров, ближе к нативному вебу, честное поведение API и настоящие веб-компоненты — кажется, у React появился достойный конкурент.


Прежде чем перейти к сути статьи, немного предыстории: я работаю с React почти восемь лет и любил каждую секунду. На нём я делал Open source, приложения и всё, что между этими крайностями.

Недавно я присоединился к команде TanStack, чтобы работать над TanStack Devtools, и у них Solid.js принят в качестве базы для всех проектов с UI. Я решил собраться с духом и выучить Solid.js. В технических выборах я стараюсь быть непредвзятым, так что подошёл к делу с открытым взглядом и желанием учиться новому!

Изучаю реактивность Solid.js

Я месяц работаю с Solid и всё ещё новичок, но должен признать: самым трудным для понимания оказалась именно реактивность. В мире React вы привыкли к такой модели:

  • вы создаёте компонент со стейтом и пропсами

  • стейт и пропсы меняются

  • компонент перерендеривается

Всё довольно просто! При изменении стейта или пропсов компонент ререндерится. Но в Solid этот подход перевёрнут с ног на голову: перерендеры происходят только тогда, когда вы явно этого хотите — через встроенную реактивность (например, сигналы). То есть если вы хотите, чтобы ваш компонент перерендерился из-за изменения пропса, вам нужно явно прописать «слушатель» — через createMemo или createStore. И только тогда компонент будет перерендерён. В отличие от React, у Solid философия такая: «Я отрендерился и больше этого делать не буду, как бы ни менялись данные, пока ты мне прямо не скажешь».

Звучит ужасно для человека из мира React: если у вас куча пропсов, которые меняются, пришлось бы обернуть их все в какой-нибудь «хук», чтобы сделать их реактивными, верно?

На самом деле нет. Пропсы по умолчанию не реактивные, но становятся таковыми, если это сигналы. То есть если вы создаёте состояние в родителе на базе сигнала и передаёте его в дочерний компонент, такой проп реактивен. На практике вы получаете почти идентичный React API, только без лишних перерендеров.

Поработав с этим и помучившись, пытаясь заставить что-то перерендериться, я понял важную вещь об архитектуре Solid.js, которая даёт серьёзное преимущество перед React:

Гораздо проще добиться перерендеринга тогда, когда он вам нужен, чем добиваться того, чтобы он не происходил, когда он не нужен!!

JSX ближе к платформе!

Если вы ещё не знали: помимо TanStack я плотно вовлечён в экосистему Remix/React Router, и один из важнейших принципов, который я оттуда вынес, — «используйте платформу». По сути это сводится к: «Если есть веб-API, пользуйтесь им, а не изобретайте свои велосипеды и абстракции средствами конкретного фреймворка». Так вот, Solid.js ощущается куда ближе к платформе — объясню на примерах.

Вы когда-нибудь работали с иконками в проекте? Приходилось идти в что-то вроде lucide-react, находить подходящую иконку, копировать её и вставлять в код React — а если у вас не было какого-нибудь изощрённого пайплайна, который превращает иконки в спрайт-листы или что-то подобное, то, скорее всего, приходилось проходиться по каждому SVG и менять атрибут class на className!

Так вот: в Solid нет особых ключевых слов для JSX. Пропсы HTML-элементов необязательно писать в camelCase — можно, но не нужно. Нет «запрещённых» атрибутов вроде class или for, что держит вас ближе к самой платформе. Диалект JSX у Solid ощущается куда приятнее, чем у React, а новичкам он помогает учиться работать с платформой по настоящей спецификации HTML, а не по «придуманной» React — и это мне очень по душе!

API работают именно так, как вы ожидаете!

Пока я работал над @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 сообщество

Друзья! Эту статью перевела команда «JavaScript for Devs» — сообщества, где мы делимся практическими кейсами, инструментами для разработчиков и свежими новостями из мира Frontend. Подписывайтесь, чтобы быть в курсе и ничего не упустить!