Перейти к содержимому

26. Solid.js vs React

Привет! 👋 Solid.js и React решают одну задачу — построение реактивных UI — но делают это кардинально по-разному. Понимание этих различий поможет тебе не только выбрать правильный инструмент, но и лучше понять оба фреймворка.

Думай об этом как о сравнении двух архитектурных подходов: React — это заново перестраивать виртуальную модель здания при каждом изменении, а Solid — точечно заменять нужный кирпич без пересмотра чертежей.


React: компоненты = функции, которые вызываются повторно

Заголовок раздела «React: компоненты = функции, которые вызываются повторно»
// React
function Counter() {
const [count, setCount] = useState(0);
// ↓ Эта функция ВЫЗЫВАЕТСЯ ЗАНОВО при каждом setCount!
console.log('Counter рендерится'); // Логируется при каждом клике
return (
<div>
<p>{count}</p>
<button onClick={() => setCount(n => n + 1)}>+</button>
</div>
);
}
// Проблема: если здесь есть дорогие вычисления — они повторяются!

Solid: компонент = функция-фабрика, вызывается один раз

Заголовок раздела «Solid: компонент = функция-фабрика, вызывается один раз»
// Solid
function Counter() {
const [count, setCount] = createSignal(0);
// ↓ Эта функция вызывается ОДИН РАЗ при создании компонента
console.log('Counter создаётся'); // Логируется только один раз!
// JSX компилируется в прямые DOM-операции с реактивными связями
return (
<div>
<p>{count()}</p> {/* ← Только этот узел обновляется */}
<button onClick={() => setCount(n => n + 1)}>+</button>
</div>
);
}

КонцепцияReactSolid
СостояниеuseStatecreateSignal
Побочные эффектыuseEffectcreateEffect
ВычисленияuseMemocreateMemo
КоллбекuseCallbackнет нужды — функции не пересоздаются
RefuseRefcreateSignal или let ref
КонтекстuseContextuseContext
РедьюсерuseReducercreateReducer из @solid-primitives
// React — ЖЁСТКИЕ правила хуков:
function ReactComponent({ condition }) {
// ❌ Хуки нельзя вызывать внутри условий!
if (condition) {
const [state] = useState(0); // Ошибка! Нарушение правил хуков
}
// ❌ Нельзя вызывать в цикле
for (let i = 0; i < 3; i++) {
const [x] = useState(i); // Ошибка!
}
const [count, setCount] = useState(0); // ✅ Только на верхнем уровне
}
// Solid — НИКАКИХ правил:
function SolidComponent(props) {
// ✅ Можно вызывать где угодно!
if (props.condition) {
const [state, setState] = createSignal(0); // OK
}
for (let i = 0; i < 3; i++) {
createEffect(() => console.log(i)); // OK
}
}

// === React ===
function ReactComponent() {
// Монтирование
useEffect(() => {
console.log('Смонтирован');
return () => console.log('Размонтирован'); // Cleanup
}, []);
// При изменении prop/state
useEffect(() => {
console.log('Обновился');
}); // Без зависимостей — каждый рендер
// При изменении конкретного значения
useEffect(() => {
console.log('count изменился:', count);
}, [count]);
}
// === Solid ===
import { onMount, onCleanup, createEffect } from 'solid-js';
function SolidComponent() {
// Монтирование (один раз)
onMount(() => {
console.log('Смонтирован');
});
// Cleanup при размонтировании
onCleanup(() => {
console.log('Размонтирован');
});
// При изменении сигнала (автоматическое отслеживание зависимостей!)
createEffect(() => {
console.log('count изменился:', count());
// Нет массива зависимостей — Solid сам отслеживает
});
}

React Render Cycle:
1. state/props изменились
2. Компонент функция вызывается заново
3. Создаётся новое дерево React элементов (VDOM)
4. Reconciler сравнивает старый и новый VDOM (diffing)
5. Минимальные изменения применяются к DOM
Solid Render Cycle:
1. Сигнал изменился
2. Уведомляются ВСЕ подписчики этого сигнала
3. Каждый подписчик (memo/effect/DOM-узел) обновляется напрямую
4. Никакого VDOM, никакого diffing, никакого reconciliation

Размер (gzip):
React + ReactDOM: ~45 KB
Solid: ~7 KB (в 6 раз меньше!)
Производительность (js-framework-benchmark):
Solid: ~1.04x (почти нативный JS!)
React: ~1.48x (50% overhead над нативным)
Vue: ~1.17x
Svelte: ~1.06x
Экосистема (2024):
React: ⭐⭐⭐⭐⭐ Огромная (создана Meta, 2013)
Solid: ⭐⭐⭐ Растущая (2021, 37k stars)
NPM downloads/week:
React: ~25 миллионов
Solid: ~200 тысяч
Вакансии:
React: Тысячи вакансий
Solid: Единицы (пока что)

✅ Выбирай Solid, если:
- Производительность критична (реал-тайм, большие списки)
- Хочешь минимальный bundle size
- Любишь чистую реактивность без магии hooks
- Строишь с нуля и нет legacy кода
- Нет нужды в огромной экосистеме
- Изучаешь как работает реактивность
✅ Выбирай React, если:
- Нужна огромная экосистема (UI-библиотеки, инструменты)
- Команда уже знает React
- Нужны React Native или React Server Components
- Много legacy кода на React
- Важна максимальная найм на рынке труда
- Нужна поддержка Meta и большого сообщества

🔄 Эквивалентный код: один компонент в двух стилях

Заголовок раздела «🔄 Эквивалентный код: один компонент в двух стилях»
// === React версия ===
import { useState, useEffect, useMemo, useCallback } from 'react';
function TodoApp() {
const [todos, setTodos] = useState([]);
const [filter, setFilter] = useState('all');
const [input, setInput] = useState('');
const filtered = useMemo(() =>
todos.filter(t =>
filter === 'all' ? true :
filter === 'active' ? !t.done : t.done
),
[todos, filter]
);
const addTodo = useCallback(() => {
if (!input.trim()) return;
setTodos(prev => [...prev, { id: Date.now(), text: input, done: false }]);
setInput('');
}, [input]);
useEffect(() => {
document.title = `Todos: ${todos.length}`;
}, [todos.length]);
return (/* JSX */);
}
// === Solid версия ===
import { createSignal, createMemo, createEffect } from 'solid-js';
function TodoApp() {
const [todos, setTodos] = createSignal([]);
const [filter, setFilter] = createSignal('all');
const [input, setInput] = createSignal('');
// useMemo → createMemo (нет зависимостей — автоматически!)
const filtered = createMemo(() =>
todos().filter(t =>
filter() === 'all' ? true :
filter() === 'active' ? !t.done : t.done
)
);
// useCallback → просто функция (не пересоздаётся)
function addTodo() {
if (!input().trim()) return;
setTodos(prev => [...prev, { id: Date.now(), text: input(), done: false }]);
setInput('');
}
// useEffect → createEffect (нет зависимостей!)
createEffect(() => {
document.title = `Todos: ${todos().length}`;
});
return (/* JSX */);
}

// ❌ Реакт-мышление: деструктуризация пропсов
function Bad({ name, count }) { // Теряет реактивность!
return <div>{name}: {count}</div>;
}
// ✅ Solid: используй props напрямую
function Good(props) {
return <div>{props.name}: {props.count}</div>;
}
// ❌ Реакт-мышление: читать сигнал вне JSX без createMemo
function Bad2() {
const value = mySignal(); // Читается один раз, не реактивно вне контекста
return <div>{value}</div>; // Это работает в JSX, но...
}
// ❌ Не делай так: ранний выход из компонента при условии
function Bad3(props) {
if (!props.user) return null; // В Solid — это может работать, но...
const name = props.user.name; // ... реактивность name может нарушиться
return <div>{name}</div>;
}
// ✅ Используй Show для условного рендеринга
function Good3(props) {
return (
<Show when={props.user}>
{(user) => <div>{user().name}</div>}
</Show>
);
}
// ❌ Эффекты при первом рендере
createEffect(() => {
// В Solid createEffect запускается СИНХРОННО при создании!
// Это может быть неожиданно если ты привык к React useEffect
fetchData(); // Запустится сразу
});
// ✅ Используй onMount для действий только после монтирования
onMount(() => {
fetchData(); // Только после монтирования в DOM
});