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

6. Мемо: createMemo

Привет! 👋 Яша здесь. Мы знаем сигналы и эффекты. Пришло время третьего основного примитива — мемо. createMemo решает конкретную задачу: «вычисляй производное значение, но только когда зависимости изменились».

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


import { createSignal, createMemo } from 'solid-js';
const [count, setCount] = createSignal(5);
const [multiplier, setMultiplier] = createSignal(3);
// createMemo: пересчитывается только при изменении count() или multiplier()
const result = createMemo(() => {
console.log('Пересчёт!');
return count() * multiplier();
});
console.log(result()); // «Пересчёт!» → 15
// Читаем result() несколько раз — пересчёта нет!
console.log(result()); // (нет пересчёта) → 15
console.log(result()); // (нет пересчёта) → 15
setCount(10);
console.log(result()); // «Пересчёт!» → 30
// Снова несколько чтений — только один пересчёт
console.log(result()); // (нет пересчёта) → 30

Ключевые свойства мемо:

  • Ленивый: вычисляется при первом чтении, не при создании
  • Кэшированный: повторные вызовы result() не пересчитывают
  • Реактивный: автоматически отслеживает зависимости (как эффект)
  • Синхронный: обновляется сразу при изменении зависимостей

Это главный вопрос: когда использовать мемо, а когда достаточно обычной функции?

const [count, setCount] = createSignal(5);
const [otherSignal, setOtherSignal] = createSignal('привет');
// Обычная функция: пересчитывается каждый раз, когда вызывается
const doubled = () => count() * 2;
// В JSX:
// {doubled()} — пересчитывается при каждом рендере узла
// Но поскольку Solid обновляет DOM только при изменении зависимостей,
// doubled() будет вызван только когда изменится count()
const doubled = createMemo(() => count() * 2);
// doubled() — гарантированно возвращает кэш, если count() не менялось
// Особенно важно, если doubled() используется в нескольких местах!
const [a, setA] = createSignal(1);
const [b, setB] = createSignal(2);
// Дорогое вычисление
const expensive = createMemo(() => heavyFibonacci(a() + b()));
// expensive() читается в 10 разных местах JSX
// Без memo: heavyFibonacci вызывается 10 раз при каждом изменении
// С memo: вычисляется ОДИН раз, все 10 мест получают кэш!

Мемо можно составлять в цепочки:

import { createSignal, createMemo } from 'solid-js';
function PricingCalculator() {
const [quantity, setQuantity] = createSignal(1);
const [basePrice, setBasePrice] = createSignal(100);
const [taxRate, setTaxRate] = createSignal(0.2); // 20%
const [promoCode, setPromoCode] = createSignal('');
// Шаг 1: скидка по промокоду
const discount = createMemo(() => {
const code = promoCode();
if (code === 'SALE10') return 0.1;
if (code === 'SALE20') return 0.2;
if (code === 'HALF') return 0.5;
return 0;
});
// Шаг 2: цена с учётом скидки
const discountedPrice = createMemo(() => {
return basePrice() * (1 - discount()); // зависит от мемо!
});
// Шаг 3: итоговая сумма с налогом
const total = createMemo(() => {
return (discountedPrice() * quantity() * (1 + taxRate())).toFixed(2);
});
return (
<div>
<p>Базовая цена: {basePrice()} ₽</p>
<p>Скидка: {(discount() * 100).toFixed(0)}%</p>
<p>Цена со скидкой: {discountedPrice().toFixed(2)} ₽</p>
<p>Итого ({quantity()} шт. + налог {taxRate() * 100}%): {total()} ₽</p>
</div>
);
}

Если изменить promoCode, пересчитается только discountdiscountedPricetotal. Если изменить quantity — пересчитается только total. Максимально эффективно!


По умолчанию мемо обновляет подписчиков при изменении ссылки или значения. Можно настроить это поведение:

import { createMemo } from 'solid-js';
// Не обновлять, если значение не изменилось (по умолчанию)
const doubled = createMemo(() => count() * 2);
// Кастомная функция сравнения
const normalizedName = createMemo(
() => name().trim().toLowerCase(),
undefined,
{ equals: (prev, next) => prev === next } // не обновлять при одинаковых строках
);
// Всегда обновлять (отключить кэш)
const alwaysNew = createMemo(
() => ({ timestamp: Date.now(), value: count() }),
undefined,
{ equals: false } // всегда считается «изменённым»
);

Практический пример: нормализация данных

Заголовок раздела «Практический пример: нормализация данных»
const [rawInput, setRawInput] = createSignal(' Яша Иванов ');
// Нормализованное имя — не обновляет если результат тот же
const cleanName = createMemo(
() => rawInput().trim().toLowerCase(),
undefined,
{ equals: (a, b) => a === b }
);
setRawInput(' Яша Иванов '); // Тот же результат → подписчики НЕ обновятся
setRawInput(' яша иванов '); // cleanName = 'яша иванов' → ДА, обновятся

🔄 createMemo vs производная сигнальная функция vs createEffect

Заголовок раздела «🔄 createMemo vs производная сигнальная функция vs createEffect»
// 1. Обычная функция: пересчёт при каждом вызове
const doubled = () => count() * 2;
// Подходит для: простые вычисления, используемые в 1-2 местах
// 2. createMemo: кэш + реактивность
const doubled = createMemo(() => count() * 2);
// Подходит для: дорогие вычисления, используемые в нескольких местах
// 3. createEffect: побочные эффекты (не возвращает значение для JSX)
createEffect(() => {
console.log(count() * 2);
});
// Подходит для: sync с DOM, fetch, аналитика — не для вычисления значений!

🧩 Реальный паттерн: фильтрованный список

Заголовок раздела «🧩 Реальный паттерн: фильтрованный список»
import { createSignal, createMemo, For } from 'solid-js';
interface Product { id: number; name: string; price: number; category: string }
function ProductList() {
const [products, setProducts] = createSignal<Product[]>([
{ id: 1, name: 'Ноутбук', price: 50000, category: 'electronics' },
{ id: 2, name: 'Мышь', price: 1500, category: 'electronics' },
{ id: 3, name: 'Стол', price: 8000, category: 'furniture' },
{ id: 4, name: 'Кресло', price: 12000, category: 'furniture' },
]);
const [search, setSearch] = createSignal('');
const [category, setCategory] = createSignal('all');
const [sortBy, setSortBy] = createSignal<'name' | 'price'>('name');
// Мемо: фильтрация — пересчитывается только при изменении search или category
const filtered = createMemo(() => {
const q = search().toLowerCase();
const cat = category();
return products().filter(p =>
(cat === 'all' || p.category === cat) &&
p.name.toLowerCase().includes(q)
);
});
// Мемо: сортировка — пересчитывается при изменении filtered() или sortBy()
const sorted = createMemo(() => {
const key = sortBy();
return [...filtered()].sort((a, b) =>
key === 'price' ? a.price - b.price : a.name.localeCompare(b.name)
);
});
// Мемо: статистика — пересчитывается только при изменении filtered()
const stats = createMemo(() => ({
count: filtered().length,
total: filtered().reduce((sum, p) => sum + p.price, 0),
avg: filtered().length
? filtered().reduce((sum, p) => sum + p.price, 0) / filtered().length
: 0,
}));
return (
<div>
<input value={search()} onInput={e => setSearch(e.target.value)} placeholder="Поиск..." />
<p>Найдено: {stats().count}, итого: {stats().total} ₽</p>
<For each={sorted()}>
{(product) => <div>{product.name} — {product.price} ₽</div>}
</For>
</div>
);
}

Fibonacci: смотри, как memo кэширует вычисление, а обычная функция выполняется снова и снова: