Как сделать реактивный стейт для каждого объекта, чтобы избежать перерендеринга при каждом изменении?
Что проверяют
Понимаете ли вы, почему React перерисовывает компоненты, и умеете ли подписать каждый элемент списка только на его данные. Следом часто спрашивают, почему не помогает React.memo, если им уже всё обёрнуто.
Как отвечать
Причина обычно одна: весь массив объектов лежит в одном состоянии наверху, любое изменение создаёт новый массив, и родитель рендерит весь список. Решение в том, чтобы сделать подписку гранулярной.
- нормализация: храните объекты словарём по
id, а список рендерит только массив идентификаторов. Иммутабельное обновление одного объекта меняет ссылку только у него, остальные сохраняют прежние; - подписка по селектору: компонент элемента получает
idи сам читает свой объект, напримерuseSelector(s => s.items[id])в Redux Toolkit илиuseStore(s => s.items[id])в Zustand. Он рендерится, только когда изменилась ссылка на его объект; - атом на объект: в Jotai у каждого объекта свой атом, например через
splitAtom. Изменение одного атома трогает одного подписчика; - observable-модели: MobX с
observerили сигналы отслеживают, какие поля прочитал компонент, и перерисовывают точечно; React.memoплюс стабильные пропсы: работает, только если объект и колбэки сохраняют ссылку. Колбэк оборачивают вuseCallbackили передают элементуidи стабильныйdispatch.
Внешние сторы опираются на useSyncExternalStore, поэтому селектор и сравнение ссылок отсекают лишние рендеры. Контекст так не умеет: смена значения Context перерисовывает всех потребителей. React Compiler избавляет от ручных useMemo и useCallback, но не спасает, когда весь список подписан на весь стор.
Где спотыкаются
Предлагают useReducer как оптимизацию. Он меняет способ описания обновлений, а не количество рендеров.
Пишут селектор, который каждый раз возвращает новый объект или массив, например через filter. Сравнение по ссылке проваливается, и компонент рендерится на каждое изменение стора.
Используют индекс массива как key или мутируют объект на месте. В первом случае React путает элементы при вставке и удалении, во втором memo не видит изменения и показывает старые данные.