Предыдущие части читайте здесь: часть 1, часть 2.
Контекст и проведение рендеринга
Context API — это механизм React, позволяющий передать одно пользовательское значение в поддерево компонентов. Любой компонент внутри определенного <MyContext.Provider>
может прочитать значение из этого экземпляра контекста, не прибегая к непосредственной передаче значения в качестве пропа через каждый промежуточный компонент.
Контекст не является инструментом управления состоянием. Разработчику необходимо самостоятельно управлять значениями, передаваемыми в контекст. Обычно в этих целях данные хранятся в состоянии компонента React, и на основании этих данных конструируются значения контекста.
Основы контекста
Провайдер контекста получает один проп value
, например <MyContext.Provider value={42}>
. Дочерние компоненты могут потреблять контекст путем рендеринга потребителя контекста и предоставления рендер-пропа, например:
<MyContext.Consumer>{ (value) => <div>{value}</div>}</MyContext.Consumer>
Или можно вызвать хук useContext
в функциональном компоненте:
const value = useContext(MyContext)
Обновление значений контекста
React проверяет, получил ли провайдер контекста новое значение, когда провайдер рендерится окружающим компонентом. Если значение провайдера является новой ссылкой, React понимает, что значение изменилось и что следует обновить компоненты, потребляющие этот контекст.
Учтите, что передача нового объекта провайдеру контекста приведет к его обновлению:
function GrandchildComponent() {
const value = useContext(MyContext);
return <div>{value.a}</div>
}
function ChildComponent() {
return <GrandchildComponent />
}
function ParentComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState("text");
const contextValue = {a, b};
return (
<MyContext.Provider value={contextValue}>
<ChildComponent />
</MyContext.Provider>
)
}
В этом примере при каждом рендеринге ParentComponent
React видит, что MyContext.Provider
получает новое значение, и по мере циклического прочесывания сверху вниз будет искать компоненты, которые потребляют MyContext
. Когда провайдер контекста получает новое значение, каждый вложенный компонент, потребляющий этот контекст, будет принудительно заново отрендерен.
Хочу отметить, что с точки зрения React каждый провайдер контекста несет в себе лишь одно значение, при этом неважно, что это — объект, массив или примитивный тип данных. В настоящий момент нельзя реализовать код, позволяющий компоненту, который потребляет контекст, пропустить обновления в связи с появлением новых значений контекста, даже если его интересует только часть нового значения.
Обновления состояния, контекст и повторный рендеринг
Попробуем обобщить изложенную информацию. Нам известно, что:
вызов
setState()
приводит к постановке этого компонента в очередь рендеринга;по умолчанию React рекурсивно рендерит все дочерние компоненты;
провайдеры контекста получают значение от компонента, который их рендерит;
значение обычно передается из состояния родительского компонента.
Это означает, что по умолчанию любое обновление состояния родительского компонента, который рендерит провайдера контекста, приведет к неизбежному рендерингу всех его потомков, считывают они значение контекста или нет!
Давайте вернемся к примеру Parent/Child/Grandchild
, приведенному выше. Здесь мы видим, что компонент GrandchildComponent
будет повторно отрендерен, но не вследствие обновления контекста, а потому что отрендерился вышестоящий компонент ChildComponent
! В этом примере не предприняты какие-либо меры по оптимизации «избыточных» рендерингов, поэтому React по умолчанию рендерит ChildComponent
и GrandchildComponent
каждый раз, когда рендерится ParentComponent
. Если родитель поместит новое значение контекста в MyContext.Provider
, компонент GrandchildComponent
увидит во время рендеринга новое значение и воспользуется им, но не обновление контекста стало причиной рендеринга компонента GrandchildComponent
— это бы произошло в любом случае.
Обновления контекста и оптимизация рендеринга
Давайте усовершенствуем и оптимизируем этот пример, но в качестве усложнения добавим в конце еще GreatGrandchildComponent
:
function GreatGrandchildComponent() {
return <div>Hi</div>
}
function GrandchildComponent() {
const value = useContext(MyContext);
return (
<div>
{value.a}
<GreatGrandchildComponent />
</div>
}
function ChildComponent() {
return <GrandchildComponent />
}
const MemoizedChildComponent = React.memo(ChildComponent);
function ParentComponent() {
const [a, setA] = useState(0);
const [b, setB] = useState("text");
const contextValue = {a, b};
return (
<MyContext.Provider value={contextValue}>
<MemoizedChildComponent />
</MyContext.Provider>
)
}
Теперь при вызове setA(42) произойдет следующее:
отрендерится
ParentComponent
;будет создана новая ссылка
contextValue
;React увидит, что
MyContext.Provider
имеет новое значение контекста и что нужно обновить всех потребителейMyContext
;React попытается отрендерить
MemoizedChildComponent
, но увидит, что компонент обернут вReact.memo()
. Никакие пропсы не передаются, значит, они остались неизменными. React полностью пропустит рендеринг компонентаChildComponent
;но поскольку
MyContext.Provider
обновился, возможно, дальше могут быть компоненты, которые должны об этом узнать;React продолжает спускаться вниз и достигает
GrandchildComponent
. Он видит, чтоMyContext
считывается компонентомGrandchildComponent
, следовательно, его нужно повторно отрендерить с учетом нового значения контекста. React повторно рендеритGrandchildComponent
, исключительно по причине изменения контекста;так как
GrandchildComponent
отрендерился, React также отрендерит любые вложенные в него компоненты. А это —GreatGrandchildComponent
.
Другими словами, как сказала Софи Алперт (Sophie Alpert):
Компонент React, идущий сразу после провайдера контекста, скорее всего, должен использовать React.memo.
Благодаря этому обновления состояния в родительском компоненте не будут приводить к принудительному повторному рендерингу всех компонентов, а лишь тех участков, где считывается контекст. (Аналогичного результата можно достичь, если ParentComponent
будет рендерить <MyContext.Provider>{props.children}</MyContext.Provider>
. В этом случае используется техника «ссылки на один и тот же элемент», что позволяет избежать повторного рендеринга дочерних компонентов, а затем отрендерить <ParentComponent><ChildComponent /></ParentComponent>
уровнем выше.)
Но учтите, что, как только отрендерится GrandchildComponent
с учетом следующего значения контекста, React снова вернется к своему поведению по умолчанию и будет рекурсивно рендерить все подряд. Итак, компонент GreatGrandchildComponent
отрендерился, и если бы что-то располагалось ниже, оно бы тоже отрендерилось.
Резюме
React всегда по умолчанию рекурсивно рендерит компоненты. Если отрендерился родительский компонент, эта же участь ждет его дочерние компоненты.
Рендеринга не нужно страшиться — через этот механизм React узнает, какие ему нужно проделать изменения в DOM.
Однако рендеринг отнимает время, и «бесплодный рендеринг», который не приводит к визуальному изменению интерфейса, означает лишнюю нагрузку.
В большинстве случаев можно передавать новые ссылки в виде колбэка функций и объектов.
Функции API, наподобие
React.memo()
, позволяют пропустить ненужные операции рендеринга, если пропсы остались неизменными.Однако если всегда передавать новые ссылки как пропсы, тогда
React.memo()
никогда не пропустит рендеринг — такие значения может потребоваться мемоизировать.Контекст позволяет передать значения любым нуждающимся компонентам, независимо от глубины их вложения.
Провайдеры контекста сравнивают свое значение по ссылкам, чтобы понять, изменилось оно или нет.
Новые значения контекста форсируют повторный рендеринг всех вложенных потребителей.
Однако во многих случаях дочерний компонент все равно повторно рендерится в рамках стандартного каскадного рендеринга дочерних компонентов после родительского.
В связи с этим желательно оборачивать дочерний компонент провайдера контекста в
React.memo()
или пользоваться{props.children}
, чтобы не рендерить дерево целиком при каждом обновлении значения контекста.Если дочерний компонент рендерится с учетом нового значения контекста, React тоже продолжает каскадный рендеринг с этой точки.
Материал подготовлен в рамках курса «React.js Developer».