Предисловие
Для вашего удобства текстовый курс перенесен на отдельную платформу. Ссылка для поступления: тык
Мы настоятельно рекомендуем пользоваться именно этой платформой по ходу курса. Ссылка на текущее занятие: тык
⚠️Прежде чем переходить к заданиям - обязательно зарегистрируйтесь на курс. Иначе вас закинет на общий поток и вы не сможете взаимодействовать с сверстниками во втором модуле курса. Это не смертельно и вполне решаемо, но тем не менее.
Продвинутые хуки: useRef, мемоизация и useContext
Зачем нужен useRef
Хук useState хранит значение между рендерами, и его изменение перерисовывает компонент. Иногда нужно хранить значение, изменение которого не должно ничего перерисовывать. Для этого есть хук useRef.
import { useRef } from 'react';
export function Timer() {
const intervalRef = useRef<number | null>(null);
// ...
}
useRef(начальноеЗначение) возвращает объект с одним свойством current. React возвращает один и тот же объект при каждом рендере компонента, поэтому значение в current сохраняется между рендерами. Изменение current не вызывает рендер.
У useRef два основных применения:
доступ к DOM-элементу, чтобы перевести на него фокус, прокрутить к нему страницу или узнать его размеры;
хранение значения, которое не нужно показывать: идентификатора таймера, предыдущего значения пропса, флага выполнения запроса.
Оба применения разберём в следующих разделах.
Почитать ещё
React: Ссылки на значения через ref,
useRef
Ссылка на DOM-элемент
React сам создаёт и меняет DOM-элементы, поэтому в компонентах их обычно не ищут. Но некоторые действия можно выполнить только с настоящим элементом: перевести фокус в поле, прокрутить страницу к элементу, запустить видео.
Чтобы получить элемент, создают ref и передают его в проп ref:
import { useEffect, useRef } from 'react';
export function SearchInput() {
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} type="search" aria-label="Поиск по карточкам" />;
}
useRef<HTMLInputElement | null>(null)создаёт ref для поля ввода. До появления элемента на странице вcurrentлежитnull.Проп
ref={inputRef}просит React записать элемент вinputRef.current, когда элемент появится в DOM, и записатьnull, когда элемент исчезнет.Эффект выполняется после того, как элемент появился на странице, поэтому в нём элемент уже доступен.
export function CommentsList({ comments }: { comments: Comment[] }) {
const endRef = useRef<HTMLDivElement | null>(null);
function scrollToEnd() {
endRef.current?.scrollIntoView({ behavior: 'smooth' });
}
return (
<>
<button type="button" onClick={scrollToEnd}>К последнему комментарию</button>
{comments.map((comment) => (
<CommentItem key={comment.id} comment={comment} />
))}
<div ref={endRef} />
</>
);
}
В этом примере ref используется в обработчике нажатия: к моменту нажатия элемент уже на странице.
Не меняйте через ref то, чем управляет React: текст, классы, дочерние элементы. Если изменить textContent элемента напрямую, при следующем рендере React перезапишет изменения или перестанет понимать, что находится на странице. Через ref выполняют только действия, которых нет в JSX: фокус, прокрутку, измерение размеров, работу с библиотеками, которые не знают о React.
Почитать ещё
React: Управление DOM с помощью ref
Когда читать и менять ref
Значение ref.current читают и меняют только в эффектах и обработчиках событий. Во время рендера этого делать нельзя.
export function CardTitle({ title }: { title: string }) {
const headingRef = useRef<HTMLHeadingElement | null>(null);
// Неправильно: во время первого рендера элемента ещё нет, current равен null
const width = headingRef.current?.offsetWidth;
useEffect(() => {
// Правильно: эффект выполняется после появления элемента на странице
console.log(headingRef.current?.offsetWidth);
}, [title]);
return <h2 ref={headingRef}>{title}</h2>;
}
Причин две.
Во время рендера React ещё не обновил DOM. При первом рендере элемента нет вовсе, а при следующих в
currentлежит элемент с состоянием из прошлого рендера.Рендер должен быть чистым. Если результат рендера зависит от значения в ref, которое меняется без рендера, компонент показывает то, что было в ref в случайный момент.
TypeScript помогает не забыть про null: у ref с типом HTMLInputElement | null нельзя вызвать focus() без проверки или опциональной цепочки ?..
Если значение из элемента нужно показать на экране, например ширину заголовка, прочитайте его в эффекте и запишите в состояние. Тогда компонент перерисуется с новым значением.
Почитать ещё
React: Лучшие практики работы с ref
Ref-колбэк и react-hook-form
В проп ref можно передать не только объект из useRef, но и функцию. React вызовет её с элементом, когда элемент появится в DOM, и с null, когда элемент исчезнет. Такая функция называется ref-колбэком.
Ref-колбэк нужен, когда элемент нужен сразу нескольким получателям. Так бывает в форме на react-hook-form: register возвращает свой ref, через который библиотека получает доступ к полю. Если просто написать ref={titleRef} после {...register('title')}, свой ref заменит ref библиотеки, и она перестанет видеть поле.
Автофокус на первое поле формы карточки в ITAM Board:
import { useEffect, useRef } from 'react';
import { useForm } from 'react-hook-form';
export function CardForm({ card, onDone, onCancel }: Props) {
const { register, handleSubmit } = useForm<FormValues>({ defaultValues: toFormValues(card) });
const titleRef = useRef<HTMLInputElement | null>(null);
const titleField = register('title', {
required: 'Введите заголовок',
maxLength: { value: 120, message: 'Не длиннее 120 символов' },
});
useEffect(() => {
titleRef.current?.focus();
}, []);
return (
<form onSubmit={handleSubmit(submit)} noValidate>
<input
{...titleField}
ref={(element) => {
titleField.ref(element);
titleRef.current = element;
}}
/>
</form>
);
}
Результат
registerсохранён в переменнуюtitleField, чтобы использовать его пропсы и егоrefотдельно.Ref-колбэк передаёт элемент и библиотеке через
titleField.ref(element), и в свойtitleRef.Эффект с пустым массивом зависимостей переводит фокус, когда форма появилась на экране.
Если элемент нужен только для автофокуса, можно обойтись атрибутом autoFocus. Но ref-колбэк пригодится и в других случаях, когда элемент нужен и библиотеке, и вашему коду.
Почитать ещё
React: Ref-колбэки
react-hook-form:
register(англ.)
Ref для значений, которые не показываются
Ref подходит для значений, которые нужны коду, но не влияют на то, что видно на экране.
Пример — отложенный поиск. Пользователь печатает в поле поиска, а фильтрация срабатывает только через 300 миллисекунд после последнего нажатия клавиши. Такой приём называется дебаунсом.
import { useEffect, useRef, useState } from 'react';
export function SearchField({ onSearch }: { onSearch: (query: string) => void }) {
const [value, setValue] = useState('');
const timerRef = useRef<number | null>(null);
function handleChange(nextValue: string) {
setValue(nextValue);
if (timerRef.current !== null) {
window.clearTimeout(timerRef.current);
}
timerRef.current = window.setTimeout(() => onSearch(nextValue), 300);
}
useEffect(() => {
return () => {
if (timerRef.current !== null) {
window.clearTimeout(timerRef.current);
}
};
}, []);
return <input type="search" value={value} onChange={(event) => handleChange(event.target.value)} />;
}
Идентификатор таймера хранится в
timerRef.current. При каждом новом символе старый таймер отменяется и запускается новый.Если бы идентификатор хранился в состоянии, каждое нажатие клавиши вызывало бы лишний рендер, хотя на экране ничего не меняется.
Эффект с очисткой отменяет таймер, если компонент исчезнет раньше, чем таймер сработает.
window.setTimeoutв браузере возвращает число, поэтому тип ref —number | null.
Правило выбора: если значение влияет на то, что пользователь видит на экране, храните его в useState. Если значение нужно только коду, храните его в useRef. Значение поля value в примере показывается в поле, поэтому оно в состоянии, а идентификатор таймера — в ref.
Почитать ещё
React: Ссылки на значения через ref
ref как проп компонента
Проп ref можно передать не только тегу, но и своему компоненту. Так делают компоненты-обёртки над полями и кнопками, чтобы тот, кто их использует, мог перевести фокус на поле внутри.
В React 19 ref передаётся компоненту как обычный проп:
import type { Ref } from 'react';
type Props = {
label: string;
ref?: Ref<HTMLInputElement>;
};
export function TextInput({ label, ref }: Props) {
return (
<label>
{label}
<input ref={ref} type="text" />
</label>
);
}
const inputRef = useRef<HTMLInputElement | null>(null);
<TextInput label="Заголовок" ref={inputRef} />
<button type="button" onClick={() => inputRef.current?.focus()}>Перейти к заголовку</button>
Тип
Ref<HTMLInputElement>подходит и для объекта изuseRef, и для ref-колбэка.Компонент передаёт полученный
refнужному элементу внутри себя.
Если компонент принимает все атрибуты обычного элемента через ComponentProps<'input'>, проп ref уже входит в этот тип, и его передаёт {...props}.
В старых версиях React для передачи ref компоненту использовали функцию forwardRef. Её можно встретить в существующих проектах и документации библиотек, но в новом коде на React 19 она не нужна.
Почитать ещё
React: Передача ref компоненту
Лишние перерисовки
На десятом занятии мы разобрали, что компонент перерисовывается, когда меняется его состояние или перерисовывается родитель. Второй случай приводит к лишним перерисовкам: компонент рендерится заново, хотя его пропсы не изменились.
На доске ITAM Board это выглядит так. Состояние поиска хранится на странице доски. Пользователь вводит букву, страница перерисовывается, вместе с ней перерисовываются колонки и каждая карточка в них, хотя сами карточки не изменились.
Обычно лишние перерисовки не заметны: рендер компонента — это вызов функции, а DOM React меняет только там, где есть различия. Заниматься оптимизацией стоит, когда интерфейс действительно работает медленно: при вводе текста есть задержка, анимации дёргаются, список из сотен элементов долго обновляется.
Прежде чем что-то оптимизировать, проверьте, что именно перерисовывается. Для этого есть вкладка Profiler в расширении React Developer Tools:
Откройте вкладку Profiler и нажмите кнопку записи.
Выполните действие в интерфейсе, например введите букву в поиск.
Остановите запись.
Profiler покажет, какие компоненты перерисовались, сколько времени занял каждый рендер и почему компонент перерисовался, если в настройках расширения включить пункт «Record why each component rendered while profiling».
Простой способ без расширения — временный console.log в начале компонента. Если после ввода одной буквы в консоли появились строки для каждой карточки, карточки перерисовываются.
React предлагает три инструмента против лишней работы: memo, useMemo и useCallback. Все они основаны на сравнении значений по ссылке.
Почитать ещё
memo
Функция memo оборачивает компонент и запрещает React перерисовывать его, если пропсы не изменились.
import { memo } from 'react';
type Props = {
card: Card;
onOpen: (cardId: string) => void;
};
export const CardPreview = memo(function CardPreview({ card, onOpen }: Props) {
return (
<button type="button" className={styles.card} onClick={() => onOpen(card.id)}>
<TypeBadge type={card.type} />
<span className={styles.title}>{card.title}</span>
</button>
);
});
Когда родитель перерисовывается, React сравнивает новые пропсы компонента с предыдущими. Каждый проп сравнивается так же, как в Object.is: числа и строки — по значению, объекты, массивы и функции — по ссылке. Если все пропсы совпали, React пропускает рендер компонента и использует прошлый результат.
Функция внутри
memoназванаCardPreview. Так имя компонента видно в React Developer Tools и в сообщениях об ошибках.memoне мешает компоненту перерисовываться при изменении его собственного состояния или контекста, который он использует.
memo полезен для компонентов, которых на странице много и которые часто получают те же пропсы. В ITAM Board это превью карточки: при вводе в поиск колонки перерисовываются, а карточки, у которых не изменились пропсы, — нет.
Но memo работает, только если пропсы действительно остаются теми же. Один проп, который меняется при каждом рендере родителя, делает memo бесполезным. Как обеспечить стабильные пропсы, разберём в следующем разделе.
Почитать ещё
React:
memo
Стабильные пропсы
Проп считается изменившимся, если новое значение не равно старому по ссылке. Объект, массив или функция, которые создаются при рендере родителя, каждый раз новые, даже если их содержимое одинаковое.
export function BoardColumns({ cards }: { cards: Card[] }) {
const [openCardId, setOpenCardId] = useState<string | null>(null);
return (
<>
{/* memo не работает: стрелочная функция создаётся заново при каждом рендере */}
{cards.map((card) => (
<CardPreview key={card.id} card={card} onOpen={(id) => setOpenCardId(id)} />
))}
{/* memo работает: функция изменения состояния одна и та же между рендерами */}
{cards.map((card) => (
<CardPreview key={card.id} card={card} onOpen={setOpenCardId} />
))}
</>
);
}
Что делает пропсы стабильными:
Функции изменения состояния из
useStateне меняются между рендерами. Их можно передавать вmemo-компоненты напрямую.Объекты данных остаются теми же, если обновлять состояние иммутабельно и точечно.
cards.map((card) => (card.id === updated.id ? updated : card))создаёт новый массив, но все карточки, кроме изменённой, остаются прежними объектами.memoу этих карточек сработает.Функции и объекты, которые нужно создать в компоненте, запоминают через
useCallbackиuseMemo. Об этих хуках следующие разделы.
Что ломает стабильность:
стрелочная функция прямо в JSX:
onOpen={(id) => open(id)};объект или массив прямо в JSX:
style={{ color: 'red' }},filters={{ search }};данные с сервера, которые загружаются заново: после повторной загрузки все карточки — новые объекты.
Проверить, работает ли memo, можно тем же временным console.log в компоненте. Если после ввода буквы в поиск строки не появились, карточки не перерисовались.
Почитать ещё
useMemo
Хук useMemo запоминает результат вычисления и пересчитывает его, только когда изменились зависимости.
import { useMemo } from 'react';
export function BoardPage() {
const [cards, setCards] = useState<Card[]>([]);
const [search, setSearch] = useState('');
const [openCardId, setOpenCardId] = useState<string | null>(null);
const visibleCards = useMemo(
() => cards.filter((card) => card.title.toLowerCase().includes(search.toLowerCase())),
[cards, search],
);
return <BoardColumns cards={visibleCards} onOpenCard={setOpenCardId} />;
}
Первый аргумент — функция, которая вычисляет значение.
Второй аргумент — массив зависимостей, как у
useEffect.При первом рендере React вызывает функцию и запоминает результат. При следующих рендерах, если зависимости те же, возвращает запомненный результат, не вызывая функцию.
У useMemo два применения.
Дорогие вычисления. Если фильтрация или сортировка обрабатывает тысячи элементов, пересчитывать её при каждом рендере, например при открытии модального окна, незачем.
Стабильная ссылка на результат. filter каждый раз возвращает новый массив. Без useMemo компонент BoardColumns получал бы новый массив при каждом рендере страницы, даже если карточки и поиск не менялись. Если BoardColumns или компоненты внутри него обёрнуты в memo, или массив используется в зависимостях эффекта, стабильная ссылка важнее скорости вычисления.
Функция внутри useMemo должна быть чистой: она только вычисляет значение и ничего не меняет вокруг. Запросы и подписки в useMemo не делают, для них есть useEffect.
Почитать ещё
React:
useMemo
useCallback
Хук useCallback запоминает функцию и возвращает ту же самую функцию, пока не изменились зависимости.
import { useCallback, useState } from 'react';
export function BoardPage() {
const [openCardId, setOpenCardId] = useState<string | null>(null);
const [viewedIds, setViewedIds] = useState<string[]>([]);
const handleOpenCard = useCallback((cardId: string) => {
setOpenCardId(cardId);
setViewedIds((prev) => (prev.includes(cardId) ? prev : [...prev, cardId]));
}, []);
return <BoardColumns cards={cards} onOpenCard={handleOpenCard} />;
}
Первый аргумент — функция, второй — массив зависимостей.
Пока зависимости не изменились,
handleOpenCard— одна и та же функция при каждом рендере, иmemoу карточек сработает.Функция использует только функции изменения состояния, которые стабильны, и запись с колбэком
prev, поэтому зависимостей нет.
useCallback(fn, deps) работает так же, как useMemo(() => fn, deps). Это отдельный хук для частого случая.
useCallback полезен только в двух ситуациях:
функцию передают в компонент, обёрнутый в
memo;функция используется в зависимостях
useEffect, и эффект не должен перезапускаться при каждом рендере.
Во всех остальных случаях useCallback ничего не ускоряет. Функция всё равно создаётся при каждом рендере, просто React возвращает старую. Оборачивать каждый обработчик в useCallback не нужно: код становится длиннее, а пользы нет.
Почитать ещё
React:
useCallback
Когда мемоизация не нужна
memo, useMemo и useCallback называют мемоизацией. Мемоизация не бесплатна: React хранит прошлые значения и сравнивает зависимости при каждом рендере, а код становится длиннее и сложнее. Поэтому применяют её точечно.
Когда мемоизация не нужна:
Интерфейс работает быстро. Если Profiler не показывает проблем, оптимизировать нечего.
Вычисление дешёвое, а результат никуда не передаётся по ссылке.
const count = cards.lengthне нужно оборачивать вuseMemo.Компонент на странице один. Страница или модальное окно перерисовываются редко, и
memoдля них почти ничего не даёт.Пропсы всё равно меняются при каждом рендере. Если в
memo-компонент передаётся объект, созданный в JSX,memoтолько добавит лишнее сравнение.
Когда мемоизация полезна:
компонент повторяется в длинном списке и часто получает те же пропсы, как превью карточек на доске;
вычисление обрабатывает много данных;
результат вычисления или функция передаются в
memo-компонент или в зависимости эффекта.
Мемоизация не должна быть способом исправить ошибку. Если эффект запускается бесконечно, и useMemo «помогает», проблема в самом эффекте, и её нужно исправить там.
Команда React выпустила React Compiler — инструмент сборки, который анализирует компоненты и добавляет мемоизацию автоматически. В проектах с компилятором memo, useMemo и useCallback пишут реже. Но понимать, почему компонент перерисовался и что такое стабильная ссылка, нужно и при работе с компилятором.
Почитать ещё
React: React Compiler
Передача данных через много уровней
Данные в React передаются через пропсы от родителя к детям. Если значение нужно компоненту глубоко внутри дерева, его передают через каждый промежуточный компонент, даже если самим промежуточным компонентам это значение не нужно.
Представьте, что фильтры доски хранятся на странице, а нужны полю поиска в панели инструментов и колонкам, которые показывают отфильтрованные карточки:
BoardPage хранит filters и setFilters
├── Toolbar передаёт filters дальше
│ └── CardFilters использует filters и setFilters
└── BoardContent передаёт filters дальше
└── BoardColumns использует filters
Компоненты Toolbar и BoardContent получают filters только для того, чтобы передать их дальше. Такая передача называется prop drilling, «протаскивание пропсов». У неё есть недостатки:
промежуточные компоненты зависят от данных, которые им не нужны;
при добавлении нового фильтра приходится менять типы пропсов всей цепочки;
компоненты труднее переносить в другое место дерева.
Протаскивание пропсов через один-два уровня — нормальная практика: по пропсам сразу видно, откуда пришли данные. Когда уровней становится больше, или значение нужно многим компонентам в разных частях дерева, используют контекст.
Почитать ещё
React: Проблема передачи пропсов
Контекст
Контекст позволяет компоненту передать значение всем компонентам внутри себя на любой глубине без пропсов.
Работа с контекстом состоит из трёх шагов.
1. Создать контекст.
import { createContext } from 'react';
import type { CardFilters } from '@/entities/card';
type FiltersContextValue = {
filters: CardFilters;
setFilters: (changes: Partial<CardFilters>) => void;
};
export const FiltersContext = createContext<FiltersContextValue | null>(null);
createContext принимает значение по умолчанию. Его получат компоненты, над которыми нет провайдера. Значение по умолчанию null делает ошибку явной: компонент вне провайдера получит null, а не пустые фильтры, которые выглядели бы как рабочие.
2. Передать значение провайдером.
export function FiltersProvider({ children }: { children: ReactNode }) {
const [filters, setAllFilters] = useState<CardFilters>(DEFAULT_FILTERS);
function setFilters(changes: Partial<CardFilters>) {
setAllFilters((prev) => ({ ...prev, ...changes }));
}
return <FiltersContext value={{ filters, setFilters }}>{children}</FiltersContext>;
}
В React 19 сам контекст используется как компонент-провайдер: <FiltersContext value={...}>. В старых версиях писали <FiltersContext.Provider value={...}>, и такая запись встречается в документации библиотек.
3. Прочитать значение.
import { useContext } from 'react';
export function CardFilters() {
const value = useContext(FiltersContext);
if (!value) {
return null;
}
const { filters, setFilters } = value;
return (
<input
type="search"
value={filters.search}
onChange={(event) => setFilters({ search: event.target.value })}
/>
);
}
useContext ищет ближайший провайдер этого контекста выше по дереву и возвращает его значение. Когда значение провайдера меняется, компонент перерисовывается.
Провайдер размещают там, где живут данные. Фильтры нужны только доске, поэтому провайдер оборачивает содержимое страницы доски, а не всё приложение.
Почитать ещё
React: Передача данных вглубь через контекст,
useContext
Собственный хук для контекста
Проверку на null не хочется повторять в каждом компоненте, который читает контекст. Поэтому чтение контекста прячут в собственный хук.
// src/features/filter-cards/model/filters-context.tsx
import { createContext, useContext, useState, type ReactNode } from 'react';
import { DEFAULT_FILTERS, type CardFilters } from '@/entities/card';
type FiltersContextValue = {
filters: CardFilters;
setFilters: (changes: Partial<CardFilters>) => void;
};
const FiltersContext = createContext<FiltersContextValue | null>(null);
export function FiltersProvider({ children }: { children: ReactNode }) {
const [filters, setAllFilters] = useState<CardFilters>(DEFAULT_FILTERS);
function setFilters(changes: Partial<CardFilters>) {
setAllFilters((prev) => ({ ...prev, ...changes }));
}
return <FiltersContext value={{ filters, setFilters }}>{children}</FiltersContext>;
}
export function useFilters() {
const value = useContext(FiltersContext);
if (!value) {
throw new Error('useFilters нужно вызывать внутри FiltersProvider');
}
return value;
}
export function CardFilters() {
const { filters, setFilters } = useFilters();
// ...
}
Собственный хук — обычная функция, имя которой начинается с
use, и которая вызывает другие хуки. К ней применяются те же правила хуков.После проверки
if (!value)TypeScript знает, чтоvalueнеnull, и хук возвращает значение нужного типа.Если компонент случайно окажется вне провайдера, приложение сразу упадёт с понятным сообщением, а не с ошибкой «Cannot read properties of null» где-то глубоко в коде.
Сам контекст не экспортируется. Снаружи модуля доступны только провайдер и хук, поэтому прочитать контекст в обход проверки невозможно.
Страница доски делится на две части: обёртку с провайдером и содержимое, которое читает фильтры. Хук useFilters нельзя вызвать в том же компоненте, который рендерит провайдер: провайдер находится внутри этого компонента, а не над ним.
export function BoardPage() {
return (
<FiltersProvider>
<BoardContent />
</FiltersProvider>
);
}
function BoardContent() {
const { filters } = useFilters();
const visibleCards = useMemo(() => filterCards(cards, filters), [cards, filters]);
// ...
}
Почитать ещё
React: Собственные хуки
Как контекст перерисовывает компоненты
Когда значение провайдера меняется, React перерисовывает все компоненты, которые читают этот контекст, даже если им нужна только часть значения.
В контексте фильтров лежат и filters, и setFilters. Пользователь вводит букву в поиск, filters меняются, и вместе с полем поиска перерисовываются все компоненты, которые вызвали useFilters, в том числе те, кому нужна только функция setFilters.
Для фильтров это не проблема: их читают несколько компонентов, и меняются они только при действиях пользователя. Для данных, которые меняются часто и нужны многим компонентам, например для списка карточек с голосами, это становится заметно. Это одна из причин, по которой на следующем занятии данные переедут в стор.
Вторая особенность связана с самим значением. Запись value={{ filters, setFilters }} создаёт новый объект при каждом рендере провайдера. Если провайдер перерисовывается по другой причине, например из-за изменения своего родителя, все потребители перерисуются, хотя фильтры не менялись. Чтобы этого избежать, значение запоминают:
export function FiltersProvider({ children }: { children: ReactNode }) {
const [filters, setAllFilters] = useState<CardFilters>(DEFAULT_FILTERS);
const setFilters = useCallback((changes: Partial<CardFilters>) => {
setAllFilters((prev) => ({ ...prev, ...changes }));
}, []);
const value = useMemo(() => ({ filters, setFilters }), [filters, setFilters]);
return <FiltersContext value={value}>{children}</FiltersContext>;
}
Теперь объект значения меняется, только когда меняются фильтры.
Если частям приложения нужны разные части значения, контекст делят на несколько. Например, отдельный контекст для данных и отдельный для функций их изменения: функции не меняются, и компоненты, которым нужны только функции, не будут перерисовываться.
Почитать ещё
Контекст или пропсы
Контекст удобен, но делает зависимости компонента менее заметными: по пропсам компонента уже не видно, какие данные ему нужны. Поэтому по умолчанию данные передают через пропсы, а контекст используют, когда для него есть причина.
Используйте пропсы, если:
значение нужно одному-двум компонентам на соседних уровнях;
промежуточные компоненты сами используют это значение;
важно, чтобы по компоненту было видно, от чего он зависит.
Используйте контекст, если:
значение нужно многим компонентам на разной глубине;
промежуточным компонентам значение не нужно, они только передают его дальше;
значение меняется редко или у него немного потребителей.
Типичные примеры данных для контекста: тема оформления, язык интерфейса, текущий пользователь, настройки, которые нужны всему разделу приложения.
Перед тем как добавить контекст, попробуйте другой способ избавиться от протаскивания пропсов: передавать компоненты через children. Например, страница может отрисовать <Toolbar><CardFilters filters={filters} onChange={setFilters} /></Toolbar>. Тогда Toolbar не получает фильтры, а просто показывает то, что ему передали.
Один контекст — одна задача. Не объединяйте тему, пользователя и фильтры в общий «контекст приложения»: изменение любой части перерисует всех, кто читает любую другую.