1. Краткое введение: Signals, Context, Zustand, MobX
Angular 19 с signals — это не просто эволюция, а попытка сделать реактивность встроенной и удобной «из коробки». В мире React же подходов к глобальному состоянию и подпискам — как велосипедов у программиста: у каждого свой, и все считают, что их велосипед самый быстрый и красивый.
Сравнение важно, чтобы:
- понять, когда и зачем использовать signals в Angular;
- увидеть, какие задачи решают похожие инструменты в React;
- понять, есть ли смысл «тащить» сторонние библиотеки или всё уже есть в фреймворке;
- не путаться в терминах и не устраивать holy war на собеседованиях.
Signals в Angular 19
Что это: минималистичный, реактивный способ хранить и менять значения, на которые компоненты могут подписываться.
Как работает: объявляете сигнал, компоненты или вычисления автоматически реагируют на его изменения.
import { signal } from '@angular/core';
export const counter = signal(0);
// где-то в компоненте
counter.set(counter() + 1);
React Context API
Что это: механизм для передачи данных через дерево компонентов без необходимости явно прокидывать пропсы на каждом уровне.
Как работает: создаёте контекст, провайдер оборачивает компонент, потребители получают доступ к значению через useContext.
const CounterContext = React.createContext(0);
// В провайдере
<CounterContext.Provider value={counter}>
<App />
</CounterContext.Provider>
// В потребителе
const counter = useContext(CounterContext);
Zustand
Что это: минималистичная библиотека для глобального состояния в React. Использует подписки, не требует провайдеров.
Как работает: создаёте стор, используете хук для доступа к данным и обновлениям. Нет лишних ререндеров.
import create from 'zustand';
const useStore = create(set => ({
counter: 0,
increment: () => set(state => ({ counter: state.counter + 1 }))
}));
// В компоненте:
const counter = useStore(state => state.counter);
MobX
Что это: реактивная библиотека для управления состоянием, основанная на наблюдаемых (observable) данных и автоматических реакциях.
Как работает: создаёте observable-объекты, компоненты автоматически обновляются при изменении данных.
import { makeAutoObservable } from 'mobx';
class CounterStore {
counter = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.counter++;
}
}
2. Signals и React Context: сходства и различия
Сходства
- Передача данных через дерево компонентов.
- Signals можно использовать в Angular-сервисах, которые доступны всему приложению через DI (dependency injection).
- Context в React позволяет избежать «проброса» пропсов и даёт доступ к данным любому компоненту-потребителю.
- Реакция на изменения.
- В обоих случаях компоненты автоматически реагируют на изменения состояния (в Angular — через сигнал, в React — через обновление контекста).
Отличия
- Гранулярность обновлений.
Signals: подписка происходит на конкретный сигнал, и только компоненты, использующие этот сигнал, обновляются.
Context: при изменении значения контекста все потребители ререндерятся, даже если используют только часть данных (до React 18 включительно, без оптимизаций). - Производительность.
Signals оптимизированы так, что изменения не вызывают лишних ререндеров, а только там, где реально используется сигнал.
Context может вызывать каскадные ререндеры, если не использовать мемоизацию или отдельные контексты для разных данных. - Сложность использования.
Signals: просто объявить и использовать, DI всё делает за вас.
Context: нужно создавать контекст, провайдер, оборачивать дерево, использовать хук. - Область видимости.
Signals в сервисе доступны везде, где сервис инжектируется.
Context работает только в пределах дерева, обёрнутого провайдером.
Пример: глобальный счётчик
Angular + Signals
// counter.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class CounterService {
counter = signal(0);
increment() {
this.counter.set(this.counter() + 1);
}
}
// В компоненте:
constructor(public counterService: CounterService) {}
increment() {
this.counterService.increment();
}
React + Context
// CounterContext.js
const CounterContext = React.createContext();
function CounterProvider({ children }) {
const [counter, setCounter] = React.useState(0);
const increment = () => setCounter(c => c + 1);
return (
<CounterContext.Provider value={{ counter, increment }}>
{children}
</CounterContext.Provider>
);
}
// В компоненте:
const { counter, increment } = React.useContext(CounterContext);
3. Signals и Zustand: реактивность без магии
Сходства
- Глобальное состояние без проброса пропсов.
- Подписки на отдельные части состояния.
- Минимум «обвязки»: не нужны провайдеры, можно использовать где угодно.
Отличия
- Встроенность.
Signals — часть Angular, интеграция на уровне фреймворка.
Zustand — сторонняя библиотека для React. - API.
Signals — декларативный, простой синтаксис: signal(), set(), update().
Zustand — стор, хук, селекторы. - Принцип работы.
Signals: подписка на значение, автоматическое обновление только нужных компонентов.
Zustand: подписка через селектор, компонент обновляется только если выбранное значение изменилось.
Пример: счётчик с Zustand
import create from 'zustand';
const useCounterStore = create(set => ({
counter: 0,
increment: () => set(state => ({ counter: state.counter + 1 }))
}));
// В компоненте:
const counter = useCounterStore(state => state.counter);
const increment = useCounterStore(state => state.increment);
В Angular с signals: примерно так же — сервис с сигналом, компоненты подписаны только на нужное значение.
Производительность
Zustand и signals оба позволяют избегать лишних обновлений: компонент обновится только если реально изменилась нужная часть состояния. Signals делают это на уровне фреймворка, Zustand — на уровне библиотеки.
4. Signals и MobX: реактивность «по-взрослому»
Сходства
- Реактивные данные: и signals, и MobX используют концепцию наблюдаемых (observable) данных.
- Автоматические подписки: компоненты обновляются только при изменении используемых данных.
- Минимум ручного управления подписками.
Отличия
- Встроенность и простота.
Signals — часть Angular, не требует сторонних зависимостей, декларативный API.
MobX — отдельная библиотека, требует декораторов или специальных функций, чуть больше «магии». - API и архитектура.
Signals: signal(), set(), update(), computed().
MobX: makeAutoObservable, observable, action, computed, observer (для компонентов). - Гранулярность подписки.
Оба решения очень эффективны: компонент обновляется только если реально использует изменённые данные.
Пример: счётчик с MobX
import { makeAutoObservable } from 'mobx';
class CounterStore {
counter = 0;
constructor() {
makeAutoObservable(this);
}
increment() {
this.counter++;
}
}
const store = new CounterStore();
// В компоненте (React):
import { observer } from 'mobx-react-lite';
const Counter = observer(() => (
<>
<div>{store.counter}</div>
<button onClick={() => store.increment()>+</button>
</>
));
В Angular с signals: всё то же самое, только без декораторов и observer-компонентов.
5. Полезные нюансы
Особенности интеграции и типичные сценарии
- Signals идеальны для Angular-приложений, где хочется реактивности без сторонних библиотек и лишней сложности.
- Context в React удобен для простых случаев, но плохо масштабируется для больших объёмов данных.
- Zustand — выбор для тех, кто хочет простоты, производительности и минимального boilerplate.
- MobX — мощный инструмент для сложных реактивных приложений, но требует понимания концепции observable и observer.
Таблица сравнения: Signals vs Context vs Zustand vs MobX
| Критерий | Angular Signals | React Context | Zustand | MobX |
|---|---|---|---|---|
| Встроено во фреймворк | Да | Да | Нет | Нет |
| Гранулярность обновлений | Высокая | Низкая (без оптимизаций) | Высокая | Высокая |
| Простота API | Очень простое | Среднее | Простое | Среднее |
| Провайдеры | DI (автоматически) | Обязательны | Не нужны | Не нужны |
| Производительность | Отличная | Может быть низкой | Отличная | Отличная |
| Сторонние зависимости | Нет | Нет | Да | Да |
| Поддержка реактивных вычислений | Да (computed) | Нет | Частично | Да (computed) |
| Поддержка вложенных областей | Да (DI) | Да (провайдеры) | Нет | Нет |
| Легко интегрировать в существующий проект | Да | Да | Да | Да |
6. Практика: мини-пример — глобальное состояние задачи
Допустим, у нас есть приложение «Список задач». Мы хотим, чтобы компоненты автоматически реагировали на добавление/удаление задач.
Angular + Signals
// task.service.ts
import { Injectable, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class TaskService {
tasks = signal<string[]>([]);
addTask(task: string) {
this.tasks.update(tasks => [...tasks, task]);
}
removeTask(index: number) {
this.tasks.update(tasks => tasks.filter((_, i) => i !== index));
}
}
// В компоненте:
constructor(public taskService: TaskService) {}
addTask(task: string) {
this.taskService.addTask(task);
}
React + Zustand
import create from 'zustand';
const useTaskStore = create(set => ({
tasks: [],
addTask: task => set(state => ({ tasks: [...state.tasks, task] })),
removeTask: index => set(state => ({
tasks: state.tasks.filter((_, i) => i !== index)
}))
}));
// В компоненте:
const tasks = useTaskStore(state => state.tasks);
const addTask = useTaskStore(state => state.addTask);
React + Context
const TaskContext = React.createContext();
function TaskProvider({ children }) {
const [tasks, setTasks] = React.useState([]);
const addTask = task => setTasks(ts => [...ts, task]);
const removeTask = index => setTasks(ts => ts.filter((_, i) => i !== index));
return (
<TaskContext.Provider value={{ tasks, addTask, removeTask }}>
{children}
</TaskContext.Provider>
);
}
// В компоненте:
const { tasks, addTask, removeTask } = React.useContext(TaskContext);
React + MobX
import { makeAutoObservable } from 'mobx';
class TaskStore {
tasks = [];
constructor() {
makeAutoObservable(this);
}
addTask(task) {
this.tasks.push(task);
}
removeTask(index) {
this.tasks.splice(index, 1);
}
}
const taskStore = new TaskStore();
// В компоненте:
import { observer } from 'mobx-react-lite';
const TaskList = observer(() => (
<>
{taskStore.tasks.map((t, i) => (
<div key={i}>{t}</div>
))}
</>
));
7. Типичные ошибки и нюансы
Ошибка №1: Глобальное состояние через Context без оптимизаций.
В React Context все потребители ререндерятся при любом изменении значения. Для сложных структур это может привести к лавине лишних обновлений. Signals и Zustand здесь выиграют.
Ошибка №2: Избыточное разделение сигналов в Angular.
Если вы создаёте слишком много отдельных сигналов, можно запутаться в зависимостях. Иногда лучше хранить сложный объект в одном сигнале и использовать computed для производных данных.
Ошибка №3: Использование MobX без понимания observer.
В MobX компонент не будет обновляться, если не обёрнут в observer. Новички часто забывают про это и удивляются, почему ничего не работает.
Ошибка №4: Попытка использовать signals вне DI-контекста.
В Angular signals лучше всего работают через сервисы с DI, иначе теряется удобство глобального доступа и тестирования.
Ошибка №5: Смешивание разных подходов без понимания.
Не стоит мешать signals и RxJS-сервисы без необходимости — это может привести к сложной и неочевидной архитектуре.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ