Борьба за производительность во фронтенде часто упирается в дилемму: как сделать состояние глобальным, но избежать лишних перерисовок интерфейса? Библиотека 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 станет отличным решением проблем с лишними ререндерами.
