Between Two Parens

What the Reagent Component?!

posted on

Did you know that when you write a form-1, form-2 or form-3 Reagent component they all become 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>
  }
}

While the fact that all Reagent components become class components is an interesting piece of trivia, the part that blew my mind was how they actually become class components. Once I understood this, a few other things became clearer like:

In the spirit of sharing this knowledge, the rest of this post will dig into the whys and wherefores of this transformation by breaking it down into 3 sections:

  1. A Pseudoclassical Pattern
  2. The Reagent Pattern
  3. Conclusion

A Pseudoclassical Pattern

The reason all of your Reagent components become class components is because at some point they are all going to pass through a Reagent function called create-class. But again, this is not really all that interesting. The part that is interesting to me is how create-class transforms a Reagent component into a React class component.

There are two ways we can explore this. The first is by looking into the implementation details of create-class and breaking it into its essential pieces. The second is to not look at the implementation details of create-class just yet and learn a little more about JavaScript "classes". Let's opt for the latter and begin this teachable moment with a little bit of JavaScript class history.

Prior to ES6, JavaScript did not have classes. and this made some JS developers sad because classes are a common pattern used to structure ones code and provide mechanisms for:

  • instantiation
  • inheritance
  • polymorphism

But as I said, prior to ES6 JavaScript did not have a formal syntax for "classes". This led the JavaScript community to develop 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 "programming patterns" vs. a programming languages formal syntax is that patterns are not as easy to search, you often need a deeper understanding of the language to understand why the patterns are structured in the way they are and the intent of these patterns is not self evident. In other words, patterns are often developed and disseminated through tribal knowledge.

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>
}

As I noted, the above is a valid React class component. As you can see, it's also verbose and error prone. For these reason JavaScript introduced ES6 classes to the language:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, Reagent</h1>
  }
}

If the live code sandbox was not enough, we can use JavaScript's built-in introspection tools to compare the pseudoclassical instantiation pattern to the ES6 class syntax.

Starting with the 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

and then perform the same tests against the 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

The TL;DR for the above is that as far as JavaScript is concerned, both definions of the Welcome component are children of React.Component and therefore are valid React class components.

At this point we are ready to return to Reagent's create-class function and explore it's implementation details.

The Reagent Pattern

As noted, the history lesson from the above section should provide a little insight into the tribal knowledge that is informing how create-class is being implemented. Namely this is becausse what create-class is doing is implementing a modified version of the pseudoclassical instantiation pattern. The following code snippet is a simplified version of some of the essential bits of create-class:

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 but again, in our case this cost is negligible.

Conclusion

As you can see we took a journey into the weeds. At a highlevel, the takeaway for me was that I was able to better understand some of the decision making processes that go into writing Reagent and React.

I felt that while this information is specific, it can be beneficial to both JavaScript and ClojureScript developers.

For the JavaScript developers, I imagine it is comforting to know that when you come to Reagent from React there are differences, but things are more familiar than it initially seems.

To the ClojureScript first developers, it is encouraging to know that everything is, generally speaking, React under the hood. This is a comfort because when we are stuck we should know that we can lean on the JavaScript community to better understand how we can write and optimize our Reagent code.

As a final point, this again illustrates one of Clojures super powers: It's hosted. We have the advantage of being able to learn on the host languages, use their libraries and enhance when needed.