Article summary
I’ve worked with a few teams who found React’s Contexts somewhat difficult to understand at first. This started me thinking about how best to explain them.
What I realized is
that Contexts are essentially nothing more than dynamic scope.
What’s Dynamic Scope?
As a programmer, you’re most likely very comfortable with lexical scoping, even if you aren’t aware of what it means. Essentially, with lexical scoping, variable names are unique and isolated to a particular part of the source code.
In JavaScript, variables are scoped within function definitions. The variable name
in function a
does not have anything to do with the variable name
in function b
, and they cannot possibly interact. With dynamic scoping, however, names are usually global, and, instead of having a single value, a stack of values is maintained. When an identifier is referenced, the value on the top of the stack is used.
Not many modern languages have dynamic scope. Here’s an example in Clojure, which provides dynamic scoping as an optional feature:
(def ^:dynamic *name* "Default")
(defn greet []
(println "Hello " *name*))
(greet) ;; => Hello Default
(binding [*name* "Ira"]
(greet) ;; => Hello Ira
(binding [*name* "Dynamic Scope"]
(greet)) ;; => Hello Dynamic Scope
(greet)) ;; => Hello Ira
(greet) ;; => Hello Default
Here we have the greet
function, which accepts no arguments, and the
dynamically scoped variable *name*
. (By convention, in Clojure, dynamically scoped variables are wrapped with asterisks, as a reminder that such identifiers behave differently.)
Based on this example, you should be able to infer that the stack of values is tied to call stack. After the binding is popped off the call stack, the previous value of *name*
is restored.
What About React Contexts?
For comparison, let’s return to React:
Once you squint a bit, you can see that these examples are basically identical. To declare the “dynamic variable,” you create a new Context and then refer to it lexically. To bind a new value to the variable, you render the Provider component. That new value will apply to any component that is rendered within that Provider — unless another <Name.Provider>
is rendered inside it at some point. Then whatever value is supplied will apply to components rendered within only the innermost Provider.
There is one difference: React Contexts are scoped to the components, not the call stack. Even so, it’s a minor thing in practice. As a React programmer, you’re already used to thinking heavily in terms of components, and it turns out that both computer programs and component hierarchies are simply trees.
Conclusion
Recognizing this equivalence allows you to leverage the wealth of information and lessons learned out there about dynamic scope. There are definitely appropriate times to use them, but they are no panacea. Incorrect use can lead to the easier introduction of bugs and make it hard to understand your code.
Additionally, once you’ve seen this symmetry between programming languages and React’s component hierarchy, you’ll start to see others. Error boundaries come to mind as a direct analog to exception handling. Some of these have made me think that React needs its own meta-language and compiler.
Finally, one interesting note. In Emacs Lisp, there was originally only dynamic scoping. This is because dynamic binding is useful for allowing you to customize the behavior of a function without requiring each invocation to pass in a ton of arguments.
The is the so-called “prop-drilling” problem, and attempts to avoid it are as old as Emacs.