Предыдущие части читайте здесь: часть 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».

Комментарии (1)


  1. Psychosynthesis
    18.12.2021 01:49
    -1

    Насколько я могу судить это достаточно корявое изложение официальной документации с всего лишь небольшими дополнениями...

    Я-то думал тут будет разбор как оно внутри работает.