JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Сравнение Signals с React Context, Zustand, MobX

Сравнение Signals с React Context, Zustand, MobX

Модуль 4: Node.js, Next.js и Angular
15 уровень , 9 лекция
Открыта

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-сервисы без необходимости — это может привести к сложной и неочевидной архитектуре.

1
Задача
Модуль 4: Node.js, Next.js и Angular, 15 уровень, 9 лекция
Недоступна
Сервис для генерации случайных цитат
Сервис для генерации случайных цитат
1
Задача
Модуль 4: Node.js, Next.js и Angular, 15 уровень, 9 лекция
Недоступна
Глобальный счетчик через сервис: делимся состоянием между компонентами
Глобальный счетчик через сервис: делимся состоянием между компонентами
1
Задача
Модуль 4: Node.js, Next.js и Angular, 15 уровень, 9 лекция
Недоступна
Сервис для списка задач с жизненным циклом компонента
Сервис для списка задач с жизненным циклом компонента
1
Задача
Модуль 4: Node.js, Next.js и Angular, 15 уровень, 9 лекция
Недоступна
Иерархия сервисов и область видимости: индивидуальный и общий цвет темы
Иерархия сервисов и область видимости: индивидуальный и общий цвет темы
1
Задача
Модуль 4: Node.js, Next.js и Angular, 15 уровень, 9 лекция
Недоступна
Мини-чат с сервисом и реактивностью через signal()
Мини-чат с сервисом и реактивностью через signal()
3
Опрос
Внедрение Зависимостей, 15 уровень, 9 лекция
Недоступен
Внедрение Зависимостей
Внедрение Зависимостей
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ