Обновить

React Context без лишних ререндеров: подписка на отдельные поля

Уровень сложностиСредний
Время на прочтение12 мин
Охват и читатели7.9K
Всего голосов 6: ↑6 и ↓0+8
Комментарии6

Комментарии 6

Около 8-и лет назад я открыл для себя MobX и с тех пор компоненты ререндерятся точечно и оптимально благодаря реактивности. В 2026-м на хабре люди всё ещё разбираются как оптимизировать контекст

MobX отлично решает эту задачу, и если он уже есть в проекте, то городить свой контекст незачем.

Но часто бывает, что вы работаете в чужом проекте, где уже выбран другой подход, например Redux, а вам нужно обернуть ветку, которая будет масштабироваться. В Redux такое неудобно: стор один на всё приложение, и для каждого экземпляра ветки пришлось бы заводить своё состояние внутри него, с ключом экземпляра. А второй стейт-менеджер ради одной ветки тянуть тоже не хочется.

Плюс вторая половина статьи не только про Context: механизм защиты от tearing в useSyncExternalStore нужен любому внешнему стору, а не только самописному.

Одни отговорки

Обычный приём 1. Если провайдер ререндерился по посторонней причине, например вместе с родителем, ссылка на value останется прежней, и React вообще не запустит обновление попотребителей.

Потребители всё равно обновятся из-за ререндера родителя (исключением будут только memo компоненты). И если не оборачивать value в useMemo, то контекст не вызовет повторный ререндер потребителей вслед за ререндером родителя. Поэтому в общем случае мемоизировать value бессмысленно.

Если JSX потребителей создаётся прямо в ререндерящемся компоненте, то да, они обновятся каскадом и useMemo ничего не даст. Но это не единственный сценарий. Если между провайдером и потребителем есть мемо-граница, каскад на ней останавливается. Самая частая граница даже не React.memo, а обычные
children:

function App() {                                                                                                                                                                                                                                                                                             
  return <Layout><Smt /></Layout>;                                                                                                                                                                                                                                                                           
}                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                             
function Layout({ children }) {                                                                                                                                                                                                                                                                              
  const [sidebarOpen, setSidebarOpen] = useState(false);                                                                                                                                                                                                                                                     
  return <UserProvider>{children}</UserProvider>;                                                                                                                                                                                                                                                            
}                                                                                                                                                                                                                                                                                                            
                                                                                                                                                                                                                                                                                                             
function UserProvider({ children }) {                                                                                                                                                                                                                                                                        
  const [user, setUser] = useState(null);                                                                                                                                                                                                                                                                    
  const value = { user, setUser };                                                                                                                                                                                                                                                                           
  return <Ctx value={value}>{children}</Ctx>;                                                                                                                                                                                                                                                                
}                                                                                                                                                                                                                                                                                                            

Меняем sidebarOpen. Ререндерятся Layout и UserProvider, но <Smt /> создал App, который не ререндерился. Ссылка на элемент та же, и React каскадом в Smt не пойдёт. А value получился новым объектом, поэтому все, кто внутри Smt читает Ctx, всё равно перерендерятся, теперь уже только
из-за контекста. С useMemo(() => ({ user, setUser }), [user]) этого не происходит.

С React Compiler таких границ становится ещё больше: он мемоизирует JSX сам. Так что не бессмысленно, а зависит от дерева.

Не могу отделаться от ощущения искусственности примера, и тем не менее мне нечего возразить.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации