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

4. Сигналы: основа реактивности

createSignal: основной примитив реактивности ⚡

Заголовок раздела «createSignal: основной примитив реактивности ⚡»

Привет! 👋 Яша здесь. Сегодня мы разбираем сигналы — сердце реактивности в Solid.js. Если ты понял сигналы — ты понял Solid на 50%. Всё остальное строится на них.

Сигнал в Solid — это реактивное хранилище значения. Когда значение меняется, все подписчики — эффекты, мемо, DOM-узлы — автоматически получают обновление. При этом компонент не перезапускается.


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()); // 2

TypeScript автоматически определяет тип из начального значения:

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 — это функция:

// React
const [count, setCount] = useState(0);
console.log(count); // число: 0
// В JSX: <p>{count}</p>
// Solid
const [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 или выносить в переменную:

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 (следующий урок!).


Аспект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-узел: