What the Reagent Component?!
Did you know that when you write a form-1, form-2 or form-3 Reagent component they all default to becoming React class components
?
For example, if you were to write this form-1
Reagent component:
(defn welcome []
[:h1 "Hello, friend"])
By the time Reagent passes it to React it would be the equivalent of you writing this:
class Welcome extends React.Component {
render() {
return <h1>Hello, friend</h1>
}
}
Okay, so, Reagent components become React Class Components
. Why do we care? This depth of understanding is valuable because it means we can better understand:
- JavaScript, ES6 classes and the idea behind "syntax sugar"
- React's strategy for distinguishing class and function components
- How ClojureScript interacts with JavaScript
The result of all of this "fundamental" learning is we can more effectively harness JavaScript from within ClojureScript.
A Pseudoclassical Pattern
The reason all of your Reagent components become class components
is because
all of the code you pass to Reagent is run through an internal Reagent function
called create-class.
create-class
is interesting because of how it uses JavaScript to
transform a Reagent component into something that is recognized as a React
class component. Before we look into what create-class
is doing, it's
helpful to review how "classes" work in JavaScript.
Prior to ES6, JavaScript did not have classes. and this made some JS developers sad because classes are a common pattern used to structure code and provide support for:
- instantiation
- inheritance
- polymorphism
But as I said, prior to ES6, JavaScript didn't have a formal syntax for "classes". To compensate for the lack of classes, the JavaScript community got creative and developed a series of instantiation patterns to help simulate classes.
Of all of these patterns, the pseudoclassical instantiation pattern
became one of the most popular ways to simulate a class in JavaScript. This is evidenced by the fact that many of the "first generation" JavaScript libraries and frameworks, like google closure library and backbone, are written in this style.
The reason we are going over this history is because the thing about a programming language is there are "patterns" and "syntax". The challenge with "patterns" is:
- They're disseminated culturally (tribal knowledge)
- They're difficult to identify
- They're often difficult to search
- They often require a deeper knowledge to understand how and why to use a pattern.
The last point in praticular is relevant to our conversation because patterns live in a context and assume prior knowledge. Knowledge like how well we know the context of a problem, the alternative approaches to addressing a problem, advancements in a language and so on.
The end result is that a pattern can just become a thing we do. We can forget or never know why it started in the first place or what the world could look like if we chose a different path.
For example, the most common way of writing a React class component is to use ES6 class syntax. But did you know that ES6 class syntax is little more than syntactic sugar around the pseudoclassical instantiation pattern
?
For example, you can write a valid React class component using the pseudoclassical instantiation pattern
like this:
// 1. define a function (component) called `Welcome`
function Welcome(props, context, updater) {
React.Component.call(this, props, context, updater)
return this
}
// 2. connect `Welcome` to the `React.Component` prototype
Welcome.prototype = Object.create(React.Component.prototype)
// 3. re-define the `constructor`
Object.defineProperty(Welcome.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: Welcome,
})
// 4. define your React components `render` method
Welcome.prototype.render = function render() {
return <h2>Hello, Reagent</h2>
}
While the above is a valid React Class Component
, it's also verbose and error prone. For these reasons JavaScript introduced ES6 classes to the language:
class Welcome extends React.Component {
render() {
return <h1>Hello, Reagent</h1>
}
}
For those looking for further evidence, we can support our claim that ES6 Classes
result in same thing as what the pseudoclassical instantiation pattern
produces by using JavaScript's built-in introspection tools to compare the pseudoclassical instantiation pattern
to the ES6 class
syntax.
pseudoclassical instantiation pattern:
function Welcome(props, context, updater) {
React.Component.call(this, props, context, updater)
return this
}
// ...repeat steps 2 - 4 from above before completing the rest
var welcome = new Welcome()
Welcome.prototype instanceof React.Component
// => true
Object.getPrototypeOf(Welcome.prototype) === React.Component.prototype
// => true
welcome instanceof React.Component
// => true
welcome instanceof Welcome
// => true
Object.getPrototypeOf(welcome) === Welcome.prototype
// => true
React.Component.prototype.isPrototypeOf(welcome)
// => true
Welcome.prototype.isPrototypeOf(welcome)
// => true
ES6 class
class Welcome extends React.Component {
render() {
console.log('ES6 Inheritance')
}
}
var welcome = new Welcome()
Welcome.prototype instanceof React.Component
// => true
Object.getPrototypeOf(Welcome.prototype) === React.Component.prototype
// => true
welcome instanceof React.Component
// => true
welcome instanceof Welcome
// => true
Object.getPrototypeOf(welcome) === Welcome.prototype
// => true
React.Component.prototype.isPrototypeOf(welcome)
// => true
Welcome.prototype.isPrototypeOf(welcome)
// => true
What does all of this mean? As far as JavaScript and React are concerned, both definions of the Welcome
component are valid React Class Components
.
With this in mind, lets look at Reagent's create-class
function and see what it does.
What Reagent Does
The history lesson from the above section is important because create-class
uses a modified version of the pseudoclassical instantiation pattern
. Let's take a look at what we mean.
The following code sample is a simplified version of Reagent's create-class
function:
function cmp(props, context, updater) {
React.Component.call(this, props, context, updater)
return this
}
goog.extend(cmp.prototype, React.Component.prototype, classMethods)
goog.extend(cmp, React.Component, staticMethods)
cmp.prototype.constructor = cmp
What we have above is Reagents take on the pseudoclassical instantiation pattern
with a few minor tweaks:
// 1. we copy to properties + methods of React.Component
goog.extend(cmp.prototype, React.Component.prototype, classMethods)
goog.extend(cmp, React.Component, staticMethods)
// 2. the constructor is not as "thorough"
cmp.prototype.constructor = cmp
Exploring point 1 we see that Reagent has opted to copy the properties
and methods
of React.Component
directly to the Reagent compnents we write. That is what's happening here:
goog.extend(cmp.prototype, React.Component.prototype, classMethods)
If we were using the the traditional pseudoclassical
approach we would instead do this:
cmp.prototype = Object.create(React.Component.prototype)
Thus, the difference is that Reagent's approach copies all the methods and properties from React.Component
to the cmp
prototype where as the second approach is going to link
the cmp
prototype to React.component
prototype. The benefit of linking is that each time you instantiate a Welcome
component, the Welcome
component does not need to re-create all of the React.components
methods and properties.
Exploring the second point, Reagent is doing this:
cmp.prototype.constructor = cmp
whereas with the traditional pseudoclassical
approach we would instead do this:
Object.defineProperty(Welcome.prototype, 'constructor', {
enumerable: false,
writable: true,
configurable: true,
value: Welcome,
})
The difference in the above approaches is that if we just use =
as we are doing in the Reagent version we create an enumerable
constructor. This can have an implication depending on who consumes our classes, but in our case we know that only React is going to be consuming our class components, so we can do this with relative confidence.
What is one of the more interesting results of the above two Reagent modifications? First, if React depended on JavaScript introspection to tell whether or not a component is a child of React.Component
we would not be happy campers:
Welcome.prototype instanceof React.Component
// => false...Welcome is not a child of React.Component
Object.getPrototypeOf(Welcome.prototype) === React.Component.prototype
// => false...React.component is not part of Welcomes prototype chain
welcome instanceof React.Component
// => false...Welcome is not an instance of React.Component
welcome instanceof Welcome
// => true...welcome is a child of Welcome
Object.getPrototypeOf(welcome) === Welcome.prototype
// => true...welcome is linked to Welcome prototype
console.log(React.Component.prototype.isPrototypeOf(welcome))
// => false...React.Component not linked to the prototype of React.Component
console.log(Welcome.prototype.isPrototypeOf(welcome))
// is Welcome is the ancestory?
What the above shows is that Welcome
is not a child of React.component
even though it has all the properties and methods that React.Component
has. This is why were lucky that React is smart about detecting class vs. function components.
Second, by copying
rather than linking
prototypes we could inccur a
performance cost. How much of a performance hit? In our case this cost is likely
negligible.
Conclusion
In my experience, digging into the weeds and going on these detours has been an important part of my growth as a developer. The weeds have allowed me to be a better programmer because I'm honing my ability to understand challenging topics and find answers. The result is a strange feeling of calm and comfort.
This calm and comfort shouldn't be overlooked. So much of our day-to-day is left unquestioned and unanalyzed. We let knowledge become "cultural" or "tribal". This is scary. It's scary because it leads to bad decisions because no one around us knows the whys or wherefores. Ultimately, it's a bad habit. A bad habit which is seen by some as a virtue because it would simply take too much time for to learn things ourselves. That's until you actually start doing this kind of work and spend time learning and observing and seeing that these "new things" we're seeing all the time aren't really new, but just another example of that old thing back.