Предыдущие части читайте здесь: часть 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 понимает, что значение изменилось и что следует обновить компоненты, потребляющие этот контекст. Учтите, что передача нового объекта провайдеру контекста приведет к его обновлению: В этом примере при каждом рендеринге ParentComponent React видит, что MyContext.Provider получает новое значение, и по мере циклического прочесывания сверху вниз будет искать компоненты, которые потребляют MyContext. Когда провайдер контекста получает новое значение, каждый вложенный компонент, потребляющий этот контекст, будет принудительно заново отрендерен. Хочу отметить, что с точки зрения React каждый провайдер контекста несет в себе лишь одно значение, при этом неважно, что это — объект, массив или примитивный тип данных. В настоящий момент нельзя реализовать код, позволяющий компоненту, который потребляет контекст, пропустить обновления в связи с появлением новых значений контекста, даже если его интересует только часть нового значения. Обновления состояния, контекст и повторный рендеринг Попробуем обобщить изложенную информацию. Нам известно, что: вызов setState() приводит к постановке этого компонента в очередь рендеринга; по умолчанию React рекурсивно рендерит все дочерние компоненты; провайдеры контекста получают значение от компонента, который их рендерит; значение обычно передается из состояния родительского компонента. Это означает, что по умолчанию любое обновление состояния родительского компонента, который рендерит провайдера контекста, приведет к неизбежному рендерингу всех его потомков, считывают они значение контекста или нет! Давайте вернемся к примеру Parent/Child/Grandchild, приведенному выше. Здесь мы видим, что компонент GrandchildComponent будет повторно отрендерен, но не вследствие обновления контекста, а потому что отрендерился вышестоящий компонент ChildComponent! В этом примере не предприняты какие-либо меры по оптимизации «избыточных» рендерингов, поэтому React по умолчанию рендерит ChildComponent и GrandchildComponent каждый раз, когда рендерится ParentComponent. Если родитель поместит новое значение контекста в MyContext.Provider, компонент GrandchildComponent увидит во время рендеринга новое значение и воспользуется им, но не обновление контекста стало причиной рендеринга компонента GrandchildComponent — это бы произошло в любом случае. Давайте усовершенствуем и оптимизируем этот пример, но в качестве усложнения добавим в конце еще GreatGrandchildComponent: Теперь при вызове 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». Редактор