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

6. useSignal и useStore

Реактивное состояние в Qwik основано на двух примитивах: useSignal() для простых значений и useStore() для сложных объектов. Они работают принципиально иначе, чем useState в React.

useSignal создаёт реактивный контейнер для одного значения:

import { component$, useSignal } from '@builder.io/qwik';
export const Counter = component$(() => {
const count = useSignal(0); // Начальное значение
return (
<div>
<p>Счётчик: {count.value}</p>
{/* Доступ через .value */}
<button onClick$={() => count.value++}>+</button>
<button onClick$={() => count.value--}>-</button>
</div>
);
});

Ключевое отличие от React: в React setState заменяет значение и вызывает ре-рендер всего компонента. В Qwik изменение .value точечно обновляет только те DOM-узлы, которые используют это значение.

useStore создаёт реактивный объект — аналог useReducer или глубокого useState:

import { component$, useStore } from '@builder.io/qwik';
interface AppState {
user: string;
count: number;
items: string[];
}
export const App = component$(() => {
const state = useStore<AppState>({
user: 'Алекс',
count: 0,
items: [],
});
return (
<div>
<p>Привет, {state.user}!</p>
<p>Счётчик: {state.count}</p>
<button onClick$={() => {
state.count++;
state.items.push(\`Item \${state.count}\`);
}}>
Добавить
</button>
</div>
);
});
const nested = useStore({
user: {
profile: {
name: 'Алекс',
age: 25
}
}
});
// Изменение вложенных свойств — всё реактивно!
nested.user.profile.name = 'Борис';
nested.user.profile.age++;
ХарактеристикаuseSignaluseStore
Тип данныхПримитивы, объектыОбъекты, массивы
Доступ.valueНапрямую
Сериализация✅ Автоматически✅ Автоматически
Глубокая реактивность
ПроизводительностьМаксимальнаяВысокая

В Qwik нет встроенного useMemo как в React — вместо этого можно использовать useComputed$:

import { component$, useSignal, useComputed$ } from '@builder.io/qwik';
export const PriceCalc = component$(() => {
const price = useSignal(100);
const quantity = useSignal(3);
// Вычисляемый сигнал
const total = useComputed$(() => price.value * quantity.value);
return (
<div>
<p>Цена: {price.value}</p>
<p>Количество: {quantity.value}</p>
<p>Итого: {total.value}</p>
</div>
);
});
import { component$, useSignal } from '@builder.io/qwik';
export const FocusInput = component$(() => {
const inputRef = useSignal<HTMLInputElement>();
return (
<>
<input ref={inputRef} type="text" placeholder="Введи текст..." />
<button onClick$={() => inputRef.value?.focus()}>
Фокус
</button>
</>
);
});
// Родительский компонент создаёт сигнал
export const Parent = component$(() => {
const theme = useSignal<'light' | 'dark'>('dark');
return (
<div>
<ThemeToggle theme={theme} />
<Content theme={theme} />
</div>
);
});
// Дочерний компонент получает сигнал как prop
export const ThemeToggle = component$<{ theme: Signal<string> }>(({ theme }) => {
return (
<button onClick$={() => {
theme.value = theme.value === 'dark' ? 'light' : 'dark';
}}>
Тема: {theme.value}
</button>
);
});
import { createContextId, useContext, useContextProvider } from '@builder.io/qwik';
// Создаём контекст
const ThemeContext = createContextId<Signal<string>>('theme');
// Провайдер
export const Root = component$(() => {
const theme = useSignal('dark');
useContextProvider(ThemeContext, theme);
return <App />;
});
// Потребитель
export const Button = component$(() => {
const theme = useContext(ThemeContext);
return <button class={theme.value}>Click</button>;
});