Что такое контекст c
Перейти к содержимому

Что такое контекст c

Область видимости переменных, константы

XYZ School

Область видимости, или контекст переменной — это часть кода, в пределах которого доступна данная переменная. В общем случае такая область определяется описанными ниже правилами:

Поле, также известное как переменная-член класса, находится в области видимости до тех пор, пока в этой области находится содержащий поле класс.

Локальная переменная находится в области видимости до тех пор, пока закрывающая фигурная скобка не укажет конец блока операторов или метода, в котором она объявлена.

Локальная переменная, объявленная в операторах цикла for, while или подобных им, видима в пределах тела цикла.

Конфликты областей видимости локальных переменных

Использование в больших программах одних и тех же имен переменных в разных частях программы является обычной практикой. Это нормально до тех пор, пока области видимости этих переменных не перекрываются и находятся в совершенно разных частях программы, таким образом исключая любую неоднозначность. Однако следует иметь в виду, что локальные переменные с одним и тем же именем не могут быть объявлены дважды в одном и том же контексте, поэтому вы не сможете поступить так, как показано ниже:

Рассмотрим следующий пример кода:

Важно отметить, что переменная i объявляется в этом коде два раза в пределах одного и того же метода. Это можно делать, поскольку переменные i объявляются в двух отдельных циклах, поэтому каждая из них локальна в пределах собственного цикла.

Вот другой пример:

Если вы попытаетесь скомпилировать это, то получите следующее сообщение об ошибке:

ScopeTest.cs (12,15) : error CS0136: A local variable named ‘3’ cannot be declared in this scope because it would give a different meaning to ‘j’, which is already used in a ‘parent or current’ scope to denote something else

Дело в том, что переменная j, которая определена перед началом цикла for, внутри цикла все еще находится в области видимости и не может из нее выйти до завершения метода Main(). Хотя вторая переменная j (недопустимая) объявлена в контексте цикла, этот контекст вложен в контекст метода Main(). Компилятор не может различить эти две переменных, поэтому не допустит объявления второй из них.

Конфликты областей видимости полей и локальных переменных

В некоторых случаях два идентификатора с одинаковыми именами (хотя и не совпадающими полностью уточненными именами) и одинаковой областью видимости можно различить, и тогда компилятор допускает объявление второй переменной. Причина в том, что C# делает принципиальное различие между переменными, объявленными на уровне типа (полями) и переменными, объявленными в методах (локальными). Рассмотрим следующий фрагмент кода:

Этот код компилируется, несмотря на то, что здесь в контексте метода Main() присутствуют две переменных с именем j: переменная j, определенная на уровне класса и существующая до тех пор, пока не будет уничтожен класс (когда завершится метод Main(), а вместе с ним и программа), и переменная j, определенная внутри Main(). В данном случае новая переменная с именем j, объявленная в методе Main(), скрывает переменную уровня класса с тем же именем. Поэтому когда вы запустите этот код, на дисплее будет отображено число 30.

Константы

Как следует из названия, константа — это переменная, значение которой не меняется за время ее существования. Предваряя переменную ключевым словом const при ее объявлении и инициализации, вы объявляете ее как константу:

Ниже перечислены основные характеристики констант:

Они должны инициализироваться при объявлении, и однажды присвоенные им значения никогда не могут быть изменены.

Значение константы должно быть вычислено во время компиляции. Таким образом, инициализировать константу значением, взятым из другой переменной, нельзя. Если все-таки нужно это сделать, используйте поля только для чтения.

Константы всегда неявно статические. Однако вы не должны (и фактически не можете) включать модификатор static в объявление константы.

Использование констант в программах обеспечивает, по крайней мере, три преимущества:

Константы облегчают чтение программ, заменяя «магические» числа и строки читаемыми именами, назначение которых легко понять.

Константы облегчают модификацию программ. Например, предположим, что в программе C# имеется константа SalesTax (налог с продаж), которой присвоено значение 6 процентов. Если налог с продаж когда-нибудь изменится, вы можете модифицировать все вычисления налога, просто присвоив новое значение этой константе, и не понадобится просматривать код в поисках значений и изменять каждое из них, надеясь, что оно нигде не будет пропущено.

Константы позволяют избежать ошибок в программах. Если попытаться присвоить новое значение константе где-то в другом месте программы, а не там, где она объявлена, компилятор выдаст сообщение об ошибке.

Почему контекст не является «инструментом для управления состоянием»

Нет. Это разные инструменты, делающие разные вещи и используемые в разных целях.

Является ли контекст инструментом «управления состоянием»?

Нет. Контекст — это форма внедрения зависимостей (dependency injection). Это транспортный механизм, который ничем не управляет. Любое «управление состоянием» осуществляется вручную, как правило, с помощью хуков useState()/useReducer().

Являются ли Context и useReducer() заменой Redux?

Нет. Они в чем-то похожи и частично пересекаются, но сильно отличаются в плане возможностей.

Когда следует использовать контекст?

Когда вы хотите сделать некоторые данные доступными для нескольких компонентов, но не хотите передавать эти данные в виде пропов на каждом уровне дерева компонентов.

Когда следует использовать Context и useReducer()?

Когда вам требуется управление состоянием умеренно сложного компонента в определенной части приложения.

Когда следует использовать Redux?

Redux наиболее полезен в следующих случаях:

  • Большое количество компонентов с состояниями, в которых используются одни и те же данные
  • Состояние приложение часто обновляется
  • Сложная логика обновления состояния
  • Приложение имеет среднюю или большую кодовую базу и над ним работает много людей
  • Вы хотите знать когда, почему и как обновляется состояние приложения и иметь возможность визуализировать эти изменения
  • Вам требуются более мощные возможности по управлению побочными эффектами, стабильностью (постоянством) и сериализацией данных

Понимание Context и Redux

Для правильного использования инструмента критически важно понимать:

  • Для чего он предназначен
  • Какие задачи он решает
  • Когда и зачем он был создан

Неразбериха вокруг Context и Redux связана, в первую очередь, с непониманием того, для чего данные инструменты предназначены, и какие задачи они решают. Поэтому, прежде чем говорить о том, когда их следует использовать, необходимо определить, что они из себя представляют и какие проблемы решают.

Что такое контекст?

Начнем с определения контекста из официальной документации:

«Контекст позволяет передавать данные через дерево компонентов без необходимости передавать пропы на промежуточных уровнях.

В типичном React-приложении данные передаются сверху вниз (от предка к потомку) с помощью пропов. Однако, этот способ может быть чересчур громоздким для некоторых типов пропов (например, выбранный язык, тема интерфейса), которые необходимо передавать во многие компоненты в приложении. Контекст предоставляет способ распределять такие данные между компонентами без необходимости явно передавать пропы на каждом уровне дерева компонентов».

Обратите внимание, в данном определении ни слова не говорится об «управлении», только о «передаче» и «распределении».

Текущее API контекста (React.createContext()) впервые было представлено в React 16.3 в качестве замены устаревшего API, доступного в ранних версиях React, но имеющего несколько недостатков дизайна. Одной из главных проблем являлось то, что обновления значений, переданных через контекст, могли быть «заблокированы», если компонент пропускал рендеринг через shouldComponentUpdate(). Поскольку многие компоненты прибегали к shouldComponentUpdate() в целях оптимизации, передача данных через контекст становилась бесполезной. createContext() был спроектирован для решения этой проблемы, поэтому любое обновление значения отразится на дочерних компонентах, даже если промежуточный компонент пропускает рендеринг.

Использование контекста

Использование контекста в приложении предполагает следующее:

  • Вызываем const MyContext = React.createContext() для создания экземпляра объекта контекста
  • В родительском компоненте рендерим <MyContext.Provider value=>. Это помещает некоторые данные в контекст. Эти данные могут быть чем угодно: строкой, числом, объектом, массивом, экземпляром класса, обработчиком событий и т.д.
  • Получаем значение контекста в любом компоненте внутри провайдера, вызывая const theContextValue = useContext(MyContext)

Обычно, значением контекста является состояние компонента:

После этого дочерний компонент может вызвать хук useContext() и прочитать значение контекста:

Цель и случаи использования контекста

Мы видим, что контекст, в действительности, ничем не управляет. Вместо этого, он представляет собой своего рода тоннель (pipe). Вы помещаете данные в начало (наверх) тоннеля с помощью <MyContext.Provider>, затем эти данные опускаются вниз до тех пор, пока компонет не запросит их с помощью useContext(MyContext).

Таким образом, основная цель контекста состоит в предотвращении «бурения пропов» (prop-drilling). Вместо передачи данных в виде пропов на каждом уровне дерева компонентов, любой компонент, вложенный в <MyContext.Provider>, может получить к ним доступ посредством useContext(MyContext). Это избавляет от необходимости писать код, реализующий логику передачи пропов.

Концептуально, это является формой внедрения зависимостей. Мы знаем, что потомок нуждается в данных определенного типа, но он не пытается создавать или устанавливать эти данные самостоятельно. Вместо этого, он полагается на то, что некоторый предок передаст эти данные во время выполнения (runtime).

Что такое Redux?

Вот о чем гласит определение из «Основ Redux»:

«Redux — это паттерн проектирования и библиотека для управления и обновления состояния приложения, использующая события, именуемые операциями (действиями, actions). Redux выступает в роли централизованного хранилища состояния приложения, следюущего правилам, позволяющим обеспечить предсказуемое обновление состояние.

Redux позволяет управлять „глобальным“ состоянием — состоянием, которое трубется нескольким частям приложения.

Паттерны и инструменты, предоставляемые Redux, позволяют легче определять где, когда, почему и как было обновлено состояние, и как отреагировало приложение на это изменение».

Обратите внимание, что данное описание указывает на:

  • Управление состоянием
  • Цель Redux — определение того, почему и как произошло изменение состояния

Архитектурно, Redux подчеркнуто использует принципы функционального программирования, что позволяет писать код в форме предсказуемых «функций-редукторов» (reducers), и обособлять идею «какое событие произошло» от логики, определяющей «как обновляется состояние при возгникновении данного события». В Redux также используется промежуточное программное обеспечение (middleware) как способ расширения возможностей хранилища, включая обработку побочных эффектов (side effects).

Redux также предоставляет инструменты разработчика, позволяющие изучать историю операций и изменение состояния в течение времени.

Redux и React

Сам по себе Redux не зависит от UI — вы можете использовать его с любым слоем представления (view layer) (React, Vue, Angular, ванильный JS и т.д.) либо без UI вообще.

Однако, чаще всего, Redux используется совместно с React. Библиотека React Redux — это официальный связывающий слой UI, позволяющий React-компонентам взаимодействовать с хранилищем Redux, получая значения из состояния Redux и инициализируя выполнение операций. React-Redux использует контекст в своих внутренних механизмах. Тем не менее, следует отметить, что React-Redux передает через контекст экземпляр хранилища Redux, а не текущее значение состояния! Это пример использования контекста для внедрения зависимостей. Мы знаем, что наши подключенные к Redux компоненты нуждаются во взаимодействии с хранилищем Redux, но мы не знаем или нам неважно, что это за хранилище, когда мы определяем компонент. Настоящее хранилище Redux внедряется в дерево во время выполнения с помощью компонента <Provider>, предоставляемого React-Redux.

Следовательно, React-Redux также может быть использован для предотвращения «бурения» (по причине внутреннего использования контекста). Вместо явной передачи нового значения через <MyContext.Provider>, мы можем поместить эти данные в хранилище Redux и затем получить их в нужном компоненте.

Цель и случаи использования (React-)Redux

Основное назначение Redux согласно официальной документации:

«Паттерны и инструменты, предоставляемые Redux, облегчают понимание того, когда, где, почему и как произошло изменение состояния, а также того, как на это отреагировало приложение».

Существует еще несколько причин использования Redux. Одной из таких причин является предотвращение «бурения».

Другие случаи использования:

  • Полное разделение логики управления состоянием и слоя UI
  • Распределение логики управления состоянием между разными слоями UI (например, в процессе перевода приложения с AngularJS на React)
  • Использование возможностей Redux middleware для добавления дополнительной логики при инициализации операций
  • Возможность сохранения частей состояния Redux
  • Возможность получения отчетов об ошибках, которые могут быть воспроизведены другими разработчиками
  • Возможность быстрой отладки логики и UI во время разработки

Почему контекст не является инструментом «управления состоянием»?

Состояние — это любые данные, описывающие поведение приложения. Мы можем разделить состояние на такие категории, как состояние сервера, состояние коммуникаций и локальное состояние, если хотим, но ключевым аспектом является хранение, чтение, обновление и использование данных.

«Управление состоянием — это изменение состояния в течение времени».

Таким образом, мы можем сказать, что «управление состоянием» означает следующее:

  • Сохранение начального значения
  • Получение текущего значения
  • Обновление значения

React-хуки useState() и useReducer() являются отличными примерами управления состоянием. С помощью этих хуков мы можем:

  • Сохранять начальное значение путем вызова хука
  • Получать текущее значение также посредством вызова хука
  • Обновлять значение, вызывая функцию setState() или dispatch(), соответственно
  • Узнавать об обновлении состояния благодаря повторному рендерингу компонента
  • Redux сохраняет начальное значение путем вызова корневого редуктора (root reducer), позволяет читать текущее значение с помощью store.getState(), обновлять значение с помощью store.dispatch(action) и получать уведомления об обновлении состояния через store.subscribe(listener)
  • MobX сохраняет начальное значение путем присвоения значения полю класса хранилища, позволяет читать текущее значение и обновлять его через поля хранилища и получать уведомления об обновлении состояния с помощью методов autorun() и computed()
React Context не соответствует названным критериям. Поэтому он не является инструментом управления состоянием

Как было отмечено ранее, контекст сам по себе ничего не хранит. За передачу значения, которое, обычно, зависит от состояния компонента, в контексте отвечает родительский компонент, который рендерит <MyContext.Provider>. Настоящее «управление состоянием» происходит при использовании хуков useState()/useReducer().

«Контекст — это то, как существующее состояние распределяется между компонентами. Контекст ничего не делает с состоянием».

«Полагаю, контекст — это как скрытые пропы, абстрагирующие состояние».

Все, что делает контекст, это позволяет избежать «бурения».

Сравнение Context и Redux

Сравним возможности контекста и React+Redux:

  • Context
    • Ничего не хранит и ничем не управляет
    • Работает только в компонентах React
    • Передает ниже простое (единственное) значение, которое может быть чем угодно (примитивом, объектом, классом и т.д.)
    • Позволяет читать это простое значение
    • Может использоваться для предотвращения «бурения»
    • Показывает текущее значение для компонентов Provider и Consumer в инструментах разработчика, но не показывает историю изменений этого значения
    • Обновляет «потребляющие» компоненты при изменении значения, но не позволяет пропустить обновление
    • Не предоставляет механизма для обработки побочных эффектов — отвечает только за рендеринг
    • Хранит и управляет простым значением (обычно, этим значением является объект)
    • Работает с любым UI, а также за пределами React-компонентов
    • Позволяет читать это простое значение
    • Может использоваться для предотвращения «бурения»
    • Может обновлять значение путем инициализации операций и запуска редукторов
    • Инструменты разработчика показывают историю инициализации операций и изменения состояния
    • Предоставляет возможность использования middleware для обработки побочных эффектов
    • Позволяет компонентам подписываться на обновления хранилища, извлекать определенные части состояния хранилища и контролировать повторный рендеринг компонентов

    Context и useReducer()

    Одной из проблем в дискуссии «Context против Redux» является то, что люди, зачастую, на самом деле имеют ввиду следующее: «Я использую useReducer() для управления состоянием и контекст для передачи значения». Но, вместо этого, они просто говорят: «Я использую контекст». В этом, на мой взгляд, кроется основная причина неразберихи, способствующая поддержанию мифа о том, что контекст «управляет состоянием».

    Рассмотрим комбинацию Context + useReducer(). Да, такая комбинация выглядит очень похоже на Redux + React-Redux. Обе эти комбинации имеют:

    • Сохраненное значение
    • Функцию-редуктор
    • Возможность инициализации операций
    • Возможность передачи значения и его чтения во вложенных компонентах
    • Context + useReducer() основан на передаче текущего значения через контекст. React-Redux передает через контекст текущий экземпляр хранилища Redux
    • Это означает, что когда useReducer() производит новое значение, все компоненты, подписанные на контекст, принудительно перерисовываются, даже если они используют только часть данных. Это может привести к проблемам с производительностью в зависимости от размера значения состояния, количества подписанных компонентов и частоты повторного рендеринга. При использовании React-Redux компоненты могут подписываться на определенную часть значения хранилища и перерисовываться только при изменении этой части
    • Контекст + useReducer() являются встроенными возможностями React и не могут использоваться за его пределами. Хранилище Redux не зависит от UI, поэтому может использоваться отдельно от React
    • React DevTools показывают текущее значение контекста, но не историю его изменений. Redux DevTools показывают все инициализированные операции, их содержимое (тип и полезную нагрузку, type and payload), состояние после каждой операции и разницу между состояниями
    • useReducer() не имеет middleware. Некоторые побочные эффекты можно обработать с помощью хука useEffect() в сочетании с useReducer(), я даже встречал отдельные попытки оборачивания useReducer() в нечто похожее на middleware, однако всему этому далеко до функционала и возможностей Redux middleware

    «Мое личное мнение состоит в том, что новый контекст готов к использованию для маловероятных обновлений с низкой частотой (таких как локализация или тема). Он также может использоваться во всех тех случаях, в которых использовался старый контекст, т.е. для статических значений с последующим распространением обновления по подпискам. Он не готов к использованию в качестве замены Flux-подобных „распространителей“ состояния».

    В сети существует много статей, рекомендующих настройку нескольких отдельных контекстов для разных частей состояния, что позволяет избежать ненужных повторных рендерингов и решить проблемы, связанные с областями видимости. Некоторые из постов также предлагают добавлять собственные «компоненты по выбору контекста», что требует использования сочетания React.memo(), useMemo() и аккуратного разделения кода на два контекста для каждой части приложения (одна для данных, другая для функций обновления). Безусловно, код можно писать и так, но в этом случае вы заново изобретаете React-Redux.

    Таким образом, несмотря на то, что Context + useReducer() — это легкая альтернатива Redux + React-Redux в первом приближении… эти комбинации не идентичны, контекст + useReducer() не может полностью заменить Redux!

    Выбор правильного инструмента

    Для выбора правильного инструмента очень важно понимать, какие задачи решает инструмент, а также какие задачи стоят перед вами.

    Обзор случаев использования

    • Context
      • Передача данных вложенным компонентам без «бурения»
      • Управление состоянием сложного компонента с помощью функции-редуктора
      • Управление состоянием сложного компонента с помощью функции-редуктора и передача состояния вложенным компонентам без «бурения»
      • Управление очень сложным состоянием с помощью функций-редукторов
      • Прослеживаемость того, когда, почему и как менялось состояние в течение времени
      • Желание полной изоляции логики управления состоянием от слоя UI
      • Распределение логики управления состоянием между разными слоями UI
      • Использование возможностей middleware для реализации дополнительной логики при инициализации операций
      • Возможность сохранения определенных частей состояния
      • Возможность получения воспроизводимых отчетов об ошибках
      • Возможность быстрой отладки логики и UI в процессе разработки
      • Все случаи использования Redux + возможность взаимодействия React-компонентов с хранилищем Redux

      Рекомендации

      Как же решить, что следует использовать?

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

      • Если вам требуется просто избежать «бурения», используйте контекст
      • Если у вас имеется сложное состояние, но вы не хотите использовать сторонние библиотеки, используйте контекст + useReducer()
      • Если вам требуется хорошая трассировка изменений состояния во времени, управлемый повторный рендеринг определенных компонентов, более мощные возможности обработки побочных эффектов и т.п., используйте Redux + React-Redux

      Часто можно услышать, что «использование Redux предполагает написание большого количества шаблонного кода», однако, «современный Redux» значительно облегчает изучение данного инструмента и его использование. Официальный пакет Redux Toolkit решает проблему шаблонизации, а хуки React-Redux упрощают использование Redux в компонентах React.

      Разумеется, добавление RTK и React-Redux в качестве зависимостей увеличивает «бандл» приложения по сравнению с контекстом + useReducer(), которые являются встроенными. Но преимущества такого подхода перекрывают недостатки — лучшая трассировка состояния, простая и более предсказуемая логика, улучшенная оптимизация рендринга компонентов.

      Также важно отметить, что одно не исключает другого — вы можете использовать Redux, Context и useReducer() вместе. Мы рекомендуем хранить «глобальное» состояние в Redux, а локальное — в компонентах и внимательно подходить к определению того, какая часть приложения должна храниться в Redux, а какая — в компонентах. Так что вы можете использовать Redux для хранения глобального состояния, Context + useReducer() — для хранения локального состояния, и Context — для статических значений, одновременно и в одном приложении.

      Еще раз: я не утверждаю, что все состояние приложения должно храниться в Redux или что Redux — это всегда лучшее решение. Я утверждаю лишь, что Redux — хороший выбор, существует много причин использовать Redux, и плата за его использование не так высока, как многие думают.

      Наконец, контекст и Redux не единственные в своем роде. Существует множество других инструментов, решающих иные аспекты управления состоянием. MobX — популярное решение, использующее ООП и наблюдаемые объекты (observables) для автоматического обновления зависимостей. Среди других подходов к обновлению состояния можно назвать Jotai, Recoil и Zustand. Библиотеки для работы с данными, вроде React Query, SWR, Apollo и Urql, предоставляют абстракции, упрощающие применение распространенных паттернов для работы с состоянием, кэшируемым сервером (скоро похожая библиотека (RTK Query) появится и для Redux Toolkit).

      Надеюсь, данная статья помогла вам понять разницу между контекстом и Redux, а также какой инструмент и в каких случаях следует использовать. Благодарю за внимание.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *