4. Сигналы: основа реактивности
createSignal: основной примитив реактивности ⚡
Заголовок раздела «createSignal: основной примитив реактивности ⚡»Привет! 👋 Яша здесь. Сегодня мы разбираем сигналы — сердце реактивности в Solid.js. Если ты понял сигналы — ты понял Solid на 50%. Всё остальное строится на них.
Сигнал в Solid — это реактивное хранилище значения. Когда значение меняется, все подписчики — эффекты, мемо, DOM-узлы — автоматически получают обновление. При этом компонент не перезапускается.
📦 createSignal — базовый синтаксис
Заголовок раздела «📦 createSignal — базовый синтаксис»import { createSignal } from 'solid-js';
// Создаём сигнал с начальным значениемconst [count, setCount] = createSignal(0);// ↑ геттер ↑ сеттер
// Читаем значение — вызываем геттер как функциюconsole.log(count()); // 0
// Обновляем значениеsetCount(1);console.log(count()); // 1
// Функциональное обновление (получаем предыдущее значение)setCount(prev => prev + 1);console.log(count()); // 2TypeScript автоматически определяет тип из начального значения:
const [name, setName] = createSignal('Яша'); // Signal<string>const [count, setCount] = createSignal(0); // Signal<number>const [flag, setFlag] = createSignal(false); // Signal<boolean>const [items, setItems] = createSignal<string[]>([]); // явный тип
// Чтениеconst n: string = name(); // тип выводится автоматически🔑 Геттер как функция — почему это важно?
Заголовок раздела «🔑 Геттер как функция — почему это важно?»Это главное концептуальное отличие от React. В React count — это значение. В Solid count — это функция:
// Reactconst [count, setCount] = useState(0);console.log(count); // число: 0// В JSX: <p>{count}</p>
// Solidconst [count, setCount] = createSignal(0);console.log(count()); // число: 0, вызываем как функцию!// В JSX: <p>{count()}</p>Зачем функция? Потому что Solid отслеживает реактивные зависимости через вызовы функций. Когда count() вызывается внутри createEffect, Solid регистрирует: «этот эффект зависит от count». Без вызова функции отслеживание невозможно.
createEffect(() => { // Solid видит вызов count() и знает: этот эффект зависит от сигнала count console.log('Count изменился:', count());});
// При setCount(5) — эффект выполнится автоматически🔗 Композиция сигналов
Заголовок раздела «🔗 Композиция сигналов»Сигналы можно читать внутри других сигналов, эффектов и компонентов, создавая реактивные цепочки:
import { createSignal } from 'solid-js';
function ShoppingCart() { const [quantity, setQuantity] = createSignal(1); const [price, setPrice] = createSignal(99.99); const [discount, setDiscount] = createSignal(0.1); // 10%
// Вычисляемые значения прямо в JSX (пересчитываются при изменении зависимостей) return ( <div> <p>Количество: {quantity()}</p> <p>Цена за штуку: {price()} ₽</p> <p>Скидка: {discount() * 100}%</p> {/* Вычисление прямо в JSX — пересчитывается при любом изменении */} <p>Итого: {(quantity() * price() * (1 - discount())).toFixed(2)} ₽</p> <button onClick={() => setQuantity(q => q + 1)}>+1</button> <button onClick={() => setQuantity(q => Math.max(1, q - 1))}>-1</button> </div> );}📐 Сигналы с объектами
Заголовок раздела «📐 Сигналы с объектами»Сигналы работают с любыми значениями, включая объекты и массивы:
import { createSignal } from 'solid-js';
// Сигнал с объектомconst [user, setUser] = createSignal({ name: 'Яша', age: 25 });
// ❌ Мутация НЕ триггерит обновлениеuser().name = 'Миша'; // ничего не произойдёт!
// ✅ Нужно создать новый объектsetUser({ ...user(), name: 'Миша' });
// ✅ Или через функциональное обновлениеsetUser(prev => ({ ...prev, name: 'Миша' }));Сигналы с массивами
Заголовок раздела «Сигналы с массивами»const [items, setItems] = createSignal<string[]>([]);
// ❌ push/pop не триггерят обновлениеitems().push('новый элемент'); // тихо не работает
// ✅ Создаём новый массивsetItems([...items(), 'новый элемент']);
// ✅ Функциональное обновлениеsetItems(prev => [...prev, 'новый элемент']);setItems(prev => prev.filter(item => item !== 'удалить'));setItems(prev => prev.map(item => item === 'old' ? 'new' : item));🌿 Производные значения в JSX
Заголовок раздела «🌿 Производные значения в JSX»Для простых вычислений можно считать прямо в JSX или выносить в переменную:
function App() { const [celsius, setCelsius] = createSignal(20);
// Вариант 1: прямо в JSX return <p>Фаренгейт: {celsius() * 9 / 5 + 32}°F</p>;}
// Вариант 2: функция (переиспользуемая «производная»)function App() { const [celsius, setCelsius] = createSignal(20); const fahrenheit = () => celsius() * 9 / 5 + 32; // ↑ обычная функция, читающая сигнал
return ( <div> <p>Цельсий: {celsius()}°C</p> <p>Фаренгейт: {fahrenheit()}°F</p> {/* fahrenheit() пересчитывается при изменении celsius() */} </div> );}Разница между обычной функцией-производной и createMemo:
- Обычная функция пересчитывается каждый раз при вызове
createMemoпересчитывается только при изменении зависимостей и кэширует результат
Для дешёвых вычислений — обычная функция. Для дорогих — createMemo (следующий урок!).
🔄 Сигналы vs useState — детальное сравнение
Заголовок раздела «🔄 Сигналы vs useState — детальное сравнение»| Аспект | useState (React) | createSignal (Solid) |
|---|---|---|
| Тип геттера | Значение count | Функция count() |
| Место использования | Внутри компонента-функции | Где угодно, даже вне компонента |
| Эффект обновления | Перерендер компонента | Обновление конкретных DOM-узлов |
| Closure-проблемы | Да (stale closure) | Нет (всегда актуальное значение) |
| Асинхронные обновления | Batching через React | Немедленные или через batch() |
| Глобальные сигналы | Нужен Context/Redux | Просто вынеси за пределы компонента |
Сигналы вне компонентов
Заголовок раздела «Сигналы вне компонентов»Это огромное преимущество Solid! Сигналы не привязаны к компонентам:
// signals.ts — отдельный модуль с глобальными сигналамиimport { createSignal } from 'solid-js';
export const [theme, setTheme] = createSignal<'light' | 'dark'>('dark');export const [currentUser, setCurrentUser] = createSignal<User | null>(null);export const [cartCount, setCartCount] = createSignal(0);// Используем в любом компоненте без Context!import { theme, setTheme, cartCount } from './signals';
function Header() { return ( <header> <span>Тема: {theme()}</span> <span>Корзина: {cartCount()}</span> <button onClick={() => setTheme(t => t === 'dark' ? 'light' : 'dark')}> Переключить тему </button> </header> );}⏱️ Ленивая инициализация
Заголовок раздела «⏱️ Ленивая инициализация»// Сигнал вычисляет начальное значение только один разconst [value, setValue] = createSignal(heavyComputation()); // вычисляется сразу
// Для ленивой инициализации используй функцию в createMemo или просто// откладывай создание сигнала до момента, когда он нужен🎮 Playground: интерактивная демонстрация сигналов
Заголовок раздела «🎮 Playground: интерактивная демонстрация сигналов»Три независимых сигнала — счётчик, текст и выбор цвета. Наблюдай, что каждый сигнал обновляет только свой DOM-узел: