
Предыдущие части читайте здесь: часть 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».
ссылка на оригинал статьи https://habr.com/ru/articles/595343/
Добавить комментарий