С ReactJS на SolidJS

Vladislav Lipatov
12 min readMay 19, 2023

--

Logo SolidJS

Предисловие

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

Всё описанное здесь основано на моём двухлетнем опыте использования этой библиотеки. Вероятно, некоторые высказывания могут показаться вам спорными, поэтому хочу отметить, что всё, о чём я здесь пишу, субъективно.

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

Коротко обо мне

Меня зовут Влад, я занимаюсь фронтендом уже около 6 лет. За это время я поработал как в больших компаниях (Yandex, Arrival), так и в небольших стартапах (The Bricks Inc, Own.Space). Я опробовал огромное количество инструментов и библиотек, поэтому имею представление о разных подходах. Около 5 лет я разрабатывал на React. Мне нравился сам подход этой библиотеки, потому что он давал мне большую свободу (использование чистого JS вместо специального синтаксиса, как, например, у Vue, Svelte) и контроль над кодом одновременно. Под контролем я подразумеваю то, что на выходе я получаю то, что написал: например, Vue и Svelte компилируют код, который пишет разработчик, в JS. Я был предан реакту довольно долгое время, пока я не узнал про SolidJS…

Зачем нам нужны новые технологии?

Вы можете спросить: “Зачем мне нужно изучать что-то ещё, ведь на рынке уже существуют решения, которые позволяют достаточно удобно разрабатывать интерфейсы?” Резонный вопрос, поскольку можно (но нужно ли?) обходиться одним решением. На самом деле людям вообще всё равно, какую технологию вы будете использовать: JQuery, Angular, React, Ebmer, VanillaJS — не имеет значение. Необходимо помнить, что мы делаем интерфейсы для людей, поэтому наша задача сделать их удобными, быстрыми, функциональными и полезными, чтобы наши пользователи могли быстро найти то, что им нужно, чтобы батарея на телефонах или ноутбуках работала дольше, чтобы браузер потреблял меньше памяти, позволяя людям экономить деньги на устройствах, чтобы пользователям было приятно пользоваться нашими продуктами и они возвращались к нам снова. Однако разработчики — тоже люди! Поэтому хочется снизить трудозатраты на разработку и поддержку функциональности и повысить удовлетворение разработчиков от процесса, чтобы никто не выгорал и не увольнялся. В этом случае и бизнес будет доволен, поскольку появится возможность быстро итерироваться по продукту, снизить материальные затраты на ресурсы и повысить прибыль и привлекательность продукта на рынке. Короче говоря, хочется, чтобы всем было хорошо! Конечно, это идеальный мир, но мы можем к нему стремиться, и новые технологии и подходы могут приблизить нас к нему.

SolidJS и стал для меня той самой технологией, которая облегчила мне и другим разработчикам в моей команде жизнь, упростила создание новой функциональности, уменьшив время разработки, и значительно повысила уровень удовлетворения от процесса. Бонусом мы получили отменную производительность, но об этом позже.

SolidJS — что? Ещё один фреймворк?

Я хорошо помню, что, когда только начинал свою frontend карьеру, мои глаза загорались ярким огнём после новостей о новых библиотеках или инструментах. Вышел новый “SUPER-MEGA-TOOL.JS” — срочно всё переписать на него! Наверное, многие с улыбкой узнают себя в прошлом :) Время шло, мои познания об инструментах множились, я пробовал внедрять в свои проекты новые библиотеки (особенно мне нравилось экспериментировать со state-менеджерами, поскольку я никогда не испытывал удовольствия писать reducer-функции и actions для redux — мне казалось, что можно сделать как-то проще). В конечном итоге мой энтузиазм насчёт новых инструментов утих, и я стал более осмотрительно подходить к инструментам и технологиям. В какой-то момент я остановился на связке React и Zustand (если была потребность в создании глобального хранилища).

Я большой фанат веба и всего, что происходит с вебом, поэтому я довольно часто читаю разные статьи на тему web разработки. Я убеждён, что web подобен медицине — нужно постоянно быть в курсе новых технологий, чтобы улучшать процессы разработки интерфейсов и, как следствие, улучшать сами интерфейсы, поскольку взаимодействие человека и компьютера является критически важной проблемой в наше время. Однажды вечером я наткнулся на статью о SolidJS. Меня заинтересовала новая концепция “сигналов”, поэтому я решил посмотреть, что же это такое.

Мы встречались?..

Итак, SolidJS не новая библиотека. Она находится в разработке уже более 6 лет, однако относительно недавно начала свой крестовый поход в мире frontend технологий. Эта библиотека будет ближе всего React-разработчикам, поскольку она во многом позаимствовала синтаксис React, однако сам принцип работы был кардинально пересмотрен и изменён.

Давайте разберём конкретный пример.

import { render } from "solid-js/web";
import { createSignal } from "solid-js";

function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);

return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}

render(() => <Counter />, document.getElementById("app"))

Ничего не напоминает?

Безусловно, мы видим, что React достаточно сильно повлиял на SolidJS (что является большим плюсом, о котором я расскажу позже). Различие же здесь заключается в том, что count является функцией (сигналом), а не числом. Когда мы вызываем count, то получаем последнее значение этого сигнала. Однако различия на этом не заканчиваются…

Главное отличие заключается в том, что при обновлении сигнала функция не запускается снова, то есть не происходит никакого ререндера! SolidJS не работает с виртуальным DOM, а обновляет его напрямую. Я буду лукавить, если не скажу, что доля компиляции здесь тоже есть. JSX, компилируется в более оптимизированную форму, позволяя точечно работать с DOM, то есть после компиляции кода выше мы получим следующее:

import { template as _$template } from "solid-js/web";
import { delegateEvents as _$delegateEvents } from "solid-js/web";
import { createComponent as _$createComponent } from "solid-js/web";
import { insert as _$insert } from "solid-js/web";
import { render } from "solid-js/web";
import { createSignal } from "solid-js";

const _tmpl$ = /*#__PURE__*/_$template(`<button type="button">`);

function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);
return (() => {
const _el$ = _tmpl$();
_el$.$$click = increment;
_$insert(_el$, count);
return _el$;
})();
}
render(() => _$createComponent(Counter, {}), document.getElementById("app"));
_$delegateEvents(["click"]);

Можно увидеть, что солид компилирует только JSX, не изменяя ваш код, который находится вне JSX. Это позволяет разработчикам писать знакомый код без необходимости изучения low-level контрукций библиотеки.

Basics

Рассмотрим следующий пример. Сколько раз мы увидим в консоли слово click ,если мы будем нажимать на кнопку?

function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);

console.log('click');

return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}

Только 1 раз! Получается, что функция в JS работает так, как и должна работать — только один раз. Вы уже представляете, какие возможности это открывает?

А теперь давайте добавим немного эффектов. Допустим, мы хотим выводить в консоль количество кликов, сделанных пользователем. В React мы бы захотели добавить что-то вроде:

useEffect(() => console.log(`Clicked: ${count}`), [count])

В SolidJS мы сделаем примерно то же самое:

createEffect(() => console.log(`Clicked: ${count()}`))

Подождите секунду, а где же указание зависимостей для эффекта? Дело в том, что в SolidJS вам не нужно указывать зависимости, потому что они автоматически начинают отслеживаться в тот момент, когда вы используете сигнал внутри эффекта (подробнее о том, как работает реактивность в SolidJS, вы можете прочитать здесь).

Эффекты можно очищать с помощью onCleanup.

Рассмотрим другой пример:

function Counter() {
const [count, setCount] = createSignal(1);
const increment = () => setCount(count() + 1);

const doubled = () => count() * 2;

return (
<div>
<button type="button" onClick={increment}>
{count()}
</button>
{/* Вместо doubled() мы можем написать count() * 2 */}
<span>Doubled clicks: {doubled()}</span>
</div>
);
}

Здесь мы можем видеть удвоенное количество кликов. Почему же doubled — функция? Помните, что функция Counter вызывается только один раз, поэтому если бы мы написали:

const doubled = count() * 2;

То получили бы просто число 2, которое бы не обновлялось при изменении сигнала count.

Мы можем создать мемоизированное значение:

const doubled = createMemo(() => count() * 2);

Теперь при вызове doubled() в нескольких местах значение будет посчитано лишь однажды и пересчитается только в тот момент, когда сигнал count изменится. Обратите внимание, что здесь тоже не нужно указывать зависимости: всё учитывается by-design.

Помимо простых сигналов мы можем создавать Stores.

import { createEffect } from "solid-js";
import { createStore } from "solid-js/store";

export function Form() {
const [state, setState] = createStore({
name: "Vlad",
age: 28,
});

createEffect(() => console.log(`Name: ${state.name}, age: ${state.age}`));

return (
<form>
<label>
Имя:{" "}
<input
type="text"
value={state.name}
onInput={(e) => setState({ name: e.target.value })}
/>
</label>
<label>
Возраст:{" "}
<input
type="number"
value={state.age}
onInput={(e) => setState({ age: e.target.value })}
/>
</label>
</form>
);
}

В данном примере state это Proxy объект. Благодаря этому эффект будет работать каждый раз, когда мы меняем поле age или поле name в объекте state. Помните классовые компоненты в React? Очень похоже. Только теперь можно создавать удобные формы, которые не будут ререндерить наш компонент!

Хочу обратить внимание на событие onInput. Почему не onChange? Дело в том, что onChange — это синтетическое событие реакта. Посколько SolidJS опирается на нативные события, то здесь следует использовать onInput.

State management

Предположим, что мы хотим создать глобальный store, какую библиотеку нам нужно использовать? React не предоставляет нам удобного способа работы с данными, поэтому были созданы бесчисленные state management библиотеки, которые постарались решить эту проблему. Для SolidJS вы можете использовать… SolidJS!

const [count, setCount] = createSignal(1);

function Counter() {
const increment = () => setCount(count() + 1);

return (
<button type="button" onClick={increment}>
{count()}
</button>
);
}

// Где-то внутри компонента
<Counter />
<Counter />
<Counter />

В примере выше все три кнопки будут иметь общий state. Всё потому, что мы можем использовать API SolidJS где угодно (важно не забывать потом очищать память, для этого можно создавать глобальные хранилища с помощью createRoot)!

Вы можете использовать createStore вместе с createSignal для ваших глобальных хранилищ.

Получается, что не нужно больше искать удобное решение для state management, ведь SolidJS позволяет управлять состоянием удобно и из коробки.

How to async?

А теперь самое интересное: как нам работать с асинхронностью? Для этого нам поможет createResource (аналогов в React нет) и Suspense (появился в React позже, чем в SolidJS).

Рассмотрим пример.

import { createSignal, createResource } from "solid-js";

export const AsyncExample = () => {
const [id, setId] = createSignal(1);

const [data, { mutate, refetch }] = createResource(
// Можно передать сигнал, который будет
// триггерить функцию fetcher
id,
// В функцию приходит последнее значение сигнала
async (currentId) =>
await fetch(
`https://jsonplaceholder.typicode.com/todos/${currentId}`
).then((res) => res.json())
);

return (
<section>
<div>
<Suspense fallback={<span>Loading...</span>}>{data()?.title}</Suspense>
</div>
<button type="button" onClick={() => setId((id) => id + 1)}>
Increase id
</button>
</section>
);
};

В данном случае мы сделаем запрос с id === 1 и отрендерим поле title сигнала data. Но что будет отображаться, пока данных ещё нет? Дело в том, что компонент Suspense создаёт область, в которой он отслеживает ресурсы (createResource). Пока они не зарезолвились, то мы будем видеть fallback. Первым аргументом я передаю в createResource сигнал id, от которого зависит запрос (fetcher function). Это значение подставляется в переменную currentId, и далее запрос выполняется снова.

Если нам не нужен сигнал, от которого будет зависеть fetcher function, то можно и не передавать ничего вообще. Тогда мы получим следующее:

const [data, { mutate, refetch }] = createResource(
async () =>
await fetch(
`https://jsonplaceholder.typicode.com/todos/1`
).then((res) => res.json())
);

Функции mutate и refetch позволяют осуществлять ручной контроль. Подробнее о createResource здесь.

Дальше — больше

Давайте теперь подумаем, какие возможности открывает SolidJS:

Нативные события

Если у нас нет виртуального DOM, то получается… что нет синтетических событий? Всё правильно. SolidJS работает с нативными событиями браузера, однако для улучшения производительности SolidJS делегирует события, чтобы уменьшить количество event listeners. Вы знаете, как работают события в браузере, — вы знаете, как они работают в SolidJS. Замечательно: меньше абстракций меньше забивают голову.

Можно забыть про React.memo и оптимизацию производительности и сосредоточиться на бизнес логике

Теперь производительность будет зависеть только от вашего кода, а не от библиотеки (наконец-то пригодятся алгоритмы, которые так часто спрашивают на собеседованиях :) ). Это возможно благодаря отсутствию ререндеров. Каждая функция запускается только один раз, поэтому можно создавать компоненты прямо в компонентах!

DOM элементы это DOM элементы

А это значит, что теперь вместо className мы будем использовать class.

// где-то внутри компонента
const div = <div></div>;
console.log(div); // в консоли мы увидим >HTMLDivElement

Больше нет правил хуков

Можно называть хуки как угодно, ведь это просто функции. Можно даже создавать эффекты в эффектах!

createEffect(() => {
// Если logIsNeeded в какой момент станет false
// то исчезнет подписка на signalToLog
if (logIsNeeded()) {

createEffect(() => {
// Будем логировать сигнал только тогда,
// Когда signalToLog изменится
console.log(signalToLog())
})

}
})

// Конечно, можно было бы сделать проще
createEffect(() => {
if (logIsNeeded()) {
console.log(signalToLog())
}
})

Контексты не заставляют всё поддерево ререндериться

А вот за это спасибо! Как часто мы хотели создать state высоко в дереве компонентов и хранить там данные, которые нужны по всему приложению, но отказывали себе в этом из-за проблем с производительностью?

Мало того, что вы знаете, как работать с контекстами в SolidJS (синтаксис точно такой же), вы можете безболезненно ими пользоваться!

const MyContext = createContext();

const A = () => {
const [state, setState] = createSignal(0);

return <MyContext.Provider value={{state}}>
... глубокое дерево
</MyContext.Provider>
}

const B = () => {
const {state} = useContext(MyContext);

return <div>{state()}</div>
}

Есть ещё множество полезных функций, с которыми вы можете ознакомиться здесь.

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

  • “Нужно обернуть компонент в memo, чтобы улучшить производительность, но это влечёт за собой оборачивание в useMemo/useCallback все props для этого компонента (что в свою очередь может привести к подобным ограничениям).”
  • “Я хочу передавать данные через контекст, однако изменение этих данных привёдет к ререндеру всего поддерева, что негативно отразится на производительности, поэтому мне нужно придумать какое-то решение (или изменить архитектуру), чтобы обойти это.”
  • useEffect в StrictMode в разных версиях React работает по-разному, поэтому мне нужно учитывать это. Иногда useEffect срабатывает дважды в dev mode, поэтому мне нужно каким-то образом игнорировать возможные проблемы при запросе данных.”
  • “Нужно договориться с командой, определиться с подходами и инструментами для создания global store, поскольку React не предоставляет удобного способа из коробки.”

Благодаря SolidJS мне удалось сфокусироваться именно на разработке полезной функциональности без ущерба производительности или архитектуры.

Можно ли переходить на Solid?

Бизнесу важно иметь надёжный инструмент разработки, который годится для production, а также иметь разработчиков, которые знают и любят конкретный инструмент. Благодаря похожему на React синтаксису у вас есть доступ к огромной армии React разработчиков, которые уже знают Solid (и вы тоже)!

Согласно State of JavaScript 2022: Front-end Frameworks (stateofjs.com) SolidJS уже второй год подряд лидирует в категории Retention. Вы можете ознакомиться с другими категориями по ссылке. Это говорит о том, что людям нравится использовать SolidJS.

Ниже я рассмотрю другие очень важные аспекты.

Performance

Итак, мы получаем действительно выразительный инструмент для создания интерфейсов (на самом деле не только интерфейсов). Вы можете спросить: “Зачем переходить на SolidJS, если позже появится очередной фреймворк X, который будет быстрее?”

Давайте посмотрим на производительность.

Мы видим, что SolidJS очень близок к VanillaJS, поэтому вероятность, что вам придётся переходить в будущем на другой более производительный фреймворк крайне мала. Более того, с выходом Solid 2.0 производительность вырастет ещё больше благодаря переработанной реактивной системе. И всё это 7.7 kB в GZIP.

Экосистема

Какие инструменты существуют для SolidJS? Всё, что нужно для разработки production-приложений. Огромный список есть здесь, а ниже я привёл самые важные, на мой взгляд, вещи. Да, экосистема пока не набрала таких масштабов, как у React, но она невероятно быстро растёт.

  1. Роутер
    solidjs/solid-router: A universal router for Solid inspired by Ember and React Router (github.com)
  2. Devtools
    solid-dev-tools — прекрасный инструмент для визуализации графа зависимостей.
  3. Набор реактивных примитивов
    solidjs-community/solid-primitives: A library of high-quality primitives that extend SolidJS reactivity. (github.com)
    Мы не хотим всё писать руками, верно? Было бы здорово, если бы другие разработчики написали за нас реактивные примитивы, которыми можно было пользоваться.
  4. Инструменты тестирования
    solidjs/solid-testing-library: Simple and complete Solid testing utilities that encourage good testing practices. (github.com)
  5. Интеграция со Storybook
    elite174/storybook-solid-js: The basic setup of storybook for solid-js (github.com)
  6. SSR framework
    What is SolidStart? (solidjs.com)
    Для React существует Next.JS. Для Solid существует SolidStart. И вот здесь есть небольшой неприятный момент: на дату написания статьи (19.05.2023) SolidStart находится в beta. Вы можете его использовать, однако этот факт нужно иметь в виду. Если же вам очень нужно SSR решение, то я советую обратить внимание на Astro, в котором есть интеграция для SolidJS.
  7. Solid-query (аналог react-query!)
    Solid Query | TanStack Query Docs
  8. Styled-components
    solidjs/solid-styled-components: A 1kb Styled Components library for Solid (github.com)
  9. Transition group
    solidjs-community/solid-transition-group: SolidJS components for applying animations when children elements enter or leave the DOM. (github.com)
  10. Playground
    Solid Playground (solidjs.com)
  11. UI библиотеки
    SolidJS · Reactive Javascript Library

Многие существующие frontend решения уже добавляют или добавили поддержку Solid.

Заключение

После 2 лет использования я могу сказать, что SolidJS годится для production разработки. Моя команда осталась довольна переходом на SolidJS с React, мы увеличи производительность, довольно просто мигрировали кодовую базу и сильно упростили код.

Все свои проекты я теперь делаю на Solid, потому что мне нравится писать меньше кода. Мне просто нравится, как всё работает.

Полезные ссылки

  1. Стримы Райана Ryan Carniato — YouTube
    Здесь собраны стримы от создателя Solid, на которых он в деталях объясняет не только работу Solid, но и работу других фреймворков и технологий. Очень рекомендую к просмотру (сам смотрел почти все)!
  2. Статьи Райана Ryan Carniato — DEV Community
  3. API SolidJS SolidJS · Reactive Javascript Library
    Советую читать документацию на английском языке, потому что на русском она устарела.
  4. Интерактивный tutorial SolidJS · Reactive Javascript Library
  5. Playground Solid Playground (solidjs.com)

--

--