Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Дайджесты новостей
Эдиториал-иллюстрация точечной реактивности хука useSignalSelector: отслеживающий прокси направляет сигналы обновлений только к зависимым компонентам интерфейса в обход холостых вычислений

Сигналы в React-Redux: как работает хук useSignalSelector в версии 9.4 Alpha

Борьба за производительность во фронтенде часто упирается в дилемму: как сделать состояние глобальным, но избежать лишних перерисовок интерфейса? Библиотека React-Redux долгие годы оставалась стандартом управления данными в React. Однако по мере роста экранов разработчики сталкивались с узким местом — холостым выполнением селекторов при каждом обновлении хранилища.

В альфа-версии React-Redux 9.4 ведущий мейнтейнер Марк Эриксон представил экспериментальный пакет react-redux/signals. Его ключевое нововведение — хук useSignalSelector поверх быстрого движка alien-signals. Обновление меняет механику подписки компонентов, перенося преимущества сигналов в классическую экосистему Redux.

Ловушка холостого хода: куда уходят миллисекунды при dispatch

Хук useSelector подписывается на обновления Redux-хранилища при монтировании компонента. При отправке любого действия (dispatch) Redux синхронно перезапускает каждый активный селектор в приложении, сравнивая результат со старым значением через строгое равенство (===).

В небольших проектах эти проверки незаметны. Но в масштабных интерфейсах возникает нагрузка O(N). Представьте список задач, где каждый элемент подписан на свою запись в хранилище:

import React from "react";
import { useSelector } from "react-redux";
import { RootState } from "./store";

export const TodoItemClassic: React.FC<{ id: string }> = ({ id }) => {
  // Селектор вызывается при ЛЮБОМ dispatch в приложении
  const todo = useSelector((state: RootState) => state.todos.byId[id]);

  return <li className={todo.completed ? "completed" : ""}>{todo.text}</li>;
};

Если пользователь вводит текст в поиск или сокет обновляет статус чата, сотни экземпляров TodoItemClassic синхронно вычисляют селекторы, хотя их данные не изменились. Процессор тратит время на холостые вызовы, что приводит к микрофризам интерфейса.

Реактивный трекинг: как прокси превращает чтение в точечную подписку

Хук useSignalSelector устраняет проблему благодаря отслеживающему прокси (tracking proxy). Вместо передачи сырого объекта состояния React-Redux оборачивает срез данных в прокси-оболочку.

В момент рендеринга селектор читает нужные свойства. Прокси перехватывает эти обращения и регистрирует точные пути зависимостей — например, todos.byIdid.text. Провайдер SignalProvider интегрируется с движком alien-signals. При dispatch сигнальный слой проверяет только фактически изменившиеся пути. Если изменения затронули другие ветви дерева, селектор даже не запускается:

import React from "react";
import { useSignalSelector } from "react-redux/signals";
import { RootState } from "./store";

export const TodoItemSignal: React.FC<{ id: string }> = ({ id }) => {
  // Прокси автоматически регистрирует точечные зависимости по путям
  const text = useSignalSelector((state: RootState) => state.todos.byId[id]?.text);
  const isCompleted = useSignalSelector((state: RootState) => state.todos.byId[id]?.completed);

  // Компонент не перерисовывается при изменении несвязанных срезов
  return <li className={isCompleted ? "completed" : ""}>{text}</li>;
};

Для подключения достаточно обернуть приложение в SignalProvider. Разработчикам больше не нужно вручную дробить селекторы или придумывать сложные компараторы равенства.

Уроки Alpha 1: свежие прокси и интеграция с Reselect

В Alpha 0 прокси корневого состояния переиспользовался между вызовами. Из-за этого мемоизация селекторов Reselect (createSelector), работающая по ссылкам на аргументы, возвращала результат из кэша без чтения свойств. Хук useSignalSelector фиксировал ноль зависимостей и переставал реагировать на обновления.

В релизе v9.4.0-alpha.1 дефект устранили: теперь при каждом запуске хука формируется свежий корневой прокси (fresh root proxy). Селекторы Reselect считывают поля и регистрируют зависимости в сигнальном графе, сохраняя мемоизацию:

import { createSelector } from "@reduxjs/toolkit";
import { RootState } from "./store";

const selectTodoById = (state: RootState, id: string) => state.todos.byId[id];

export const selectTodoItemStats = createSelector(
  [selectTodoById],
  (todo) => ({
    textLength: todo?.text.length ?? 0,
    isUrgent: Boolean(todo?.priority && todo.priority > 3),
  })
);

Методы массивов filter, find и map теперь возвращают чистые объекты с зависимостями по идентификаторам, предотвращая лишние срабатывания прокси при обходе коллекций.

Инженерный вердикт: когда переходить на сигналы

Пакет react-redux/signals дает экосистеме инструмент гранулярной реактивности без миграции на другие библиотеки. Накладные расходы на рантайм alien-signals составляют около 7.2 кБ в сжатом виде (gzip).

Внедрять альфа-версию в продакшен пока рано: API продолжает калиброваться, а селекторы требуют аккуратности (селектор state => state не регистрирует зависимостей). Но для проектов со сложными интерфейсами и частыми обновлениями релиз 9.4 станет отличным решением проблем с лишними ререндерами.