Почему возникает бесконечный рендер при деструктуризации Zustand store
При работе с библиотекой Zustand в React разработчики часто сталкиваются с проблемой бесконечного ререндера компонента при попытке получить несколько свойств из store через деструктуризацию массива или объекта. Это типичная ошибка, связанная с тем, как Zustand отслеживает изменения и сравнивает селекторы.
Причина бесконечного рендера
Когда вы используете селектор, возвращающий новый массив или объект при каждом вызове, Zustand считает, что состояние изменилось, и запускает ререндер. В коде ниже каждый вызов useToDoStore создаёт новый массив, что приводит к бесконечному циклу:
const [tasks, createTask, updateTask, removeTask] = useToDoStore((state) => [
state.tasks,
state.createTask,
state.updateTask,
state.removeTask,
]);Поскольку функции createTask, updateTask и removeTask не меняются между рендерами, их включение в массив не является проблемой. Основная проблема - сам факт создания нового массива на каждом рендере.
Как исправить: лучшие практики Zustand
1. Используйте отдельные вызовы useStore для каждого свойства
Самый простой и надёжный способ - вызывать useToDoStore для каждого нужного свойства отдельно. Это гарантирует, что компонент будет ререндериться только при изменении конкретного свойства:
const tasks = useToDoStore((state) => state.tasks);
const createTask = useToDoStore((state) => state.createTask);
const updateTask = useToDoStore((state) => state.updateTask);
const removeTask = useToDoStore((state) => state.removeTask);Хотя код становится длиннее, он полностью решает проблему производительности и бесконечных циклов.
2. Используйте shallow-сравнение (поверхностное сравнение)
Zustand поддерживает функцию shallow из библиотеки zustand/shallow. Она выполняет поверхностное сравнение объектов и массивов, предотвращая ложные ререндеры:
import { shallow } from 'zustand/shallow';
const { tasks, createTask, updateTask, removeTask } = useToDoStore(
(state) => ({
tasks: state.tasks,
createTask: state.createTask,
updateTask: state.updateTask,
removeTask: state.removeTask,
}),
shallow
);Этот подход компактнее и эффективнее, так как Zustand не будет создавать новый объект при каждом вызове - shallow сравнивает ключи и значения.
3. Используйте useStore с пользовательским селектором
Можно создать свою функцию сравнения, но для большинства случаев shallow достаточно. Если вам нужно больше контроля, используйте useRef для хранения предыдущего результата селектора.
Почему отдельные вызовы работают
Когда вы вызываете useToDoStore((state) => state.tasks), Zustand возвращает примитивное значение (массив задач). При следующем рендере Zustand сравнивает новое и старое значение с помощью Object.is. Если массив задач не изменился - ререндера не происходит. Функции же, такие как createTask, являются стабильными ссылками и никогда не вызывают ререндер.
Когда store большой: выбирайте shallow
Если в store десятки полей, писать отдельные вызовы для каждого - утомительно. Используйте shallow с объектом: это даёт чистый код и высокую производительность. Альтернатива - разбить store на несколько маленьких хранилищ (slices), но это усложняет архитектуру.
Заключение
Бесконечный рендер в Zustand возникает из-за создания новых ссылок в селекторе. Используйте либо отдельные вызовы useStore, либо shallow-сравнение. Оба метода решают проблему и позволяют эффективно работать с большими store.