Original text: how does – react – tell – a – class – from – a – function

How does react know if a component is a class component

Consider the Greeting component defined as a function:

function Greeting() {
  return <p>Hello</p>;
}
Copy the code

React also supports defining it as a class:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>; }}Copy the code

(Until recently, this was the only way to use features like states.)

When you want to render the
component, you don’t have to worry about how it is defined:

// Class or function
<Greeting />
Copy the code

But, as React itself, he cares about these differences!

If Greeting were a function, react would call it:

// Your code
function Greeting() {
  return <p>Hello</p>;
}

/ / the React
const result = Greeting(props); // <p>Hello</p>
Copy the code

But if Greeting were a class, React would need to instantiate it with new and then call the Render method on the instance:

// Your code
class Greeting extends React.Component {
  render() {
    return <p>Hello</p>; }}/ / the React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
Copy the code

In both cases, React’s goal is to get the render node (in this case,

Hello
).

So how does React know if something is a class or a function?

As in my previous post, you don’t need to know what this does in React. I didn’t know for years. Please don’t make it an interview question. In fact, this article is more about JavaScript than React.

This blog is for curious readers who want to know why React works a certain way. Are you the one? Then let’s dig together.

It’s been a long journey. Fasten your seat belt. This article doesn’t have much to say aboutReactInformation itself, but we will discussnew.this.class.arrow function.prototype.__ proto__.instanceofAnd how do these things liveJavaScriptSome aspects of collaborative work in. Fortunately, when you use React, you don’t have to think about that. If you are implementing React……

(If you really want to know the answer, please pull to the end.)


First, we need to understand why it is important to treat functions and classes differently. Notice how we use the new operator when calling the class:

// If Greeting is a function
const result = Greeting(props); // <p>Hello</p>

// If Greeting is a class
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
Copy the code

Let’s take a quick look at what new does in Javascript.


In the past (before ES6), Javascript didn’t have classes. However, you can use ordinary functions to represent class-like patterns. Specifically, you can use any function in a role similar to a class constructor by adding new before the call:

// just a function
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // βœ… Person {name: 'Fred'}
var george = Person('George'); // πŸ”΄ will not work as scheduled
Copy the code

You can still write code like this today! Try it out in DevTools.

If you call Person(‘Fred’) without a new,this points to global and useless things inside (for example, Windows or undefined). So our code will crash or be as stupid as setting window.name.

By adding new before the call, you say: “Hey JavaScript, I know Person is just a function, but let’s assume it’s similar to a class constructor. I can assign something like this.name by creating a {} object and pointing this to it within the Person function. And return that object to me.”

That’s what the new operator does.

var fred = new Person('Fred'); // in 'Person', the same object is' this'
Copy the code

The new operator enables the returned Fred object to use anything on Person.prototype.

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred');
fred.sayHi();
Copy the code

This is how people simulate classes before adding them directly to JavaScript.


So new has been around in JavaScript for a while. But class is new. Now let’s rewrite the above code using class to more closely match our intent:

class Person {
  constructor(name) {
    this.name = name;
  }
  sayHi() {
    alert('Hi, I am ' + this.name); }}let fred = new Person('Fred');
fred.sayHi();
Copy the code

It is important to capture developer intent in language and API design.

If you write a function, JavaScript cannot guess whether it is called like alert(), or whether it acts as a constructor like new Person(). Forgetting to specify new for functions like Person can lead to messy behavior.

Class syntax allows the equivalent to say: “This isn’t just a function – it’s a class, and it has a constructor.” If you forget to use new when calling it, JavaScript raises an error:

let fred = new Person('Fred');
// βœ… If Person is a function: works fine
// βœ… If Person is a class: works fine too

let george = Person('George'); // We forgot `new`
// 😳 If Person is a constructor-like function: confusing behavior
// πŸ”΄ If Person is a class: fails immediately
Copy the code

This helps us catch bugs early, rather than waiting for some obscure bug like this.name to be treated as window.name instead of George.name.

However, this means React needs to use new before calling any classes. It can’t just be called as a regular function, because JavaScript will treat it as an error!

class Counter extends React.Component {
  render() {
    return <p>Hello</p>; }}// πŸ”΄ React can't just do this:
const instance = Counter(props);
Copy the code

That means trouble.


Before we look at how React solves this problem, it’s important to remember that most people use compilers like Babel in React to compile modern features, such as classes from older browsers. So we need to consider the compiler in our design.

In earlier versions of Babel, classes could be called without new. However, this was fixed – by generating some extra code:

function Person(name) {
  // A bit simplified from Babel output:
  if(! (this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Our code:
  this.name = name;
}

new Person('Fred'); / / βœ… Okay
Person('George');   // πŸ”΄ Can’t call class as a function
Copy the code

You might see something like this in your packaged code, and that’s all the _classCallCheck does. (You can reduce the bundle size by choosing to go into “loose mode” without checking, but this can complicate your eventual conversion to true native classes.)


By now, you should have a rough idea of the difference between calling something with or without new:

new Person() Person()
class βœ… this is a Person instance πŸ”΄ TypeError
function βœ… this is a Person instance 😳 this is window or undefined

This is why React calls components correctly. If your component is defined as a class, React needs to use new when calling it.

So React can check if something is a class?

It’s not that easy! Even if we can tell a class with functions in JavaScript, this still doesn’t apply to classes handled by tools like Babel. To the browser, they are just simple features.


Well, maybe React could use new every time it’s called? Unfortunately, this doesn’t always work.

As regular functions, calling them with new provides them with an object instance as this. It is ideal for functions written as constructors (Person, above), but it confuses function components:

function Greeting() {
  // We don't want "this" to be an instance of either case here
  return <p>Hello</p>;
}
Copy the code

But that may be tolerable. There are two other reasons to kill the idea of using new all the time.


The first reason you can kill it is because of the arrow function. Try it:

const Greeting = () => <p>Hello</p>;
new Greeting(); // πŸ”΄ Greeting is not a constructor
Copy the code

This behavior is intentional and follows the design of the arrow function. One of the main advantages of arrow functions is that they do not have their own this binding – instead, this is solved from the nearest regular function:

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    return friends.map(friend= >
      <Friend
        // `this` is resolved from the `render` method
        size={this.props.size} name={friend.name} key={friend.id} /> ); }}Copy the code

Ok, so the arrow method doesn’t have its own this. But that means they will be completely useless as constructors!

const Person = (name) = > {
  // πŸ”΄ This wouldn’t make sense!
  this.name = name;
}
Copy the code

Therefore, JavaScript does not allow you to call arrow functions with new. If you do, you may have made a mistake anyway and it’s best to tell you earlier. This is similar to the way JavaScript does not allow classes to be called without new.

That was nice, but it also affected our plans. React cannot call new on everything because it would break the arrow function! We can try to detect the arrow function by missing prototype, not just new:

((a)= > {}).prototype // undefined
(function() {}).prototype // {constructor: f}
Copy the code

But this does not apply to functions compiled with Babel. This may not be a big deal, but there’s another reason this approach is a dead end.


Another reason we can’t always use new is that it prevents React from supporting components that return strings or other primitive types.

function Greeting() {
  return 'Hello';
}

Greeting(); / / βœ… 'Hello'
new Greeting(); / / 😳 Greeting {}
Copy the code

Again, it has to do with New’s design quirks. As we saw earlier, new tells the JavaScript engine to create an object, create it inside the function, and then take that object as the result of new.

However, JavaScript also allows functions called with new to override the return value of new by returning some other object. Presumably, this is considered useful for patterns like pools where we want to reuse instances:

// Created lazily
var zeroVector = null;

function Vector(x, y) {
  if (x === 0 && y === 0) {
    if(zeroVector ! = =null) {
      // Reuse the same instance
      return zeroVector;
    }
    zeroVector = this;
  }
  this.x = x;
  this.y = y;
}

var a = new Vector(1.1);
var b = new Vector(0.0);
var c = new Vector(0.0); // 😲 b === c
Copy the code

However, if the function is not an object, new also ignores the return value of the function entirely. If you return a string or a number, it is as if no return is displayed.

function Answer() {
  return 42;
}

Answer(); / / βœ… 42
new Answer(); / / 😳 Answer {}
Copy the code

When a function is called with new, the original return value (such as a number or string) cannot be read from the function. So if React always uses new, it won’t be able to add support components that return strings!

This is unacceptable, so we need to compromise.


What have we learned so far? React needs to call the class with new (including the Babel output), but it needs to call regular or arrow functions (including the Babel output) without new. There is no reliable way to tell them apart.

If we can’t solve the general problem, can we solve a more specific problem?

When defining components as classes, you may want to extend react.ponent for built-in methods such as this.setstate (). Can we only detect React.Component descendants instead of trying to detect all classes?

Spoiler alert: This is exactly what React does.


Perhaps the usual way to check if Greeting is the React component class is to test greeting. prototype instanceof React.Component:

class A {}
class B extends A {}

console.log(B.prototype instanceof A); // true
Copy the code

I know what you’re thinking. What just happened? ! To answer this question, we need to understand JavaScript prototypes.

You may be familiar with prototype chains. Every object in JavaScript may have a “prototype.” When we write Fred.sayhi () but the Fred object does not have the sayHi attribute, we look for the sayHi attribute on Fred’s prototype. If we don’t find it there, we’ll look at the next prototype in the chain — the prototype of Fred’s prototype. And so on.

Confusingly, the prototype property of a class or function does not point to the prototype of that value. I’m not kidding.

function Person() {}

console.log(Person.prototype); // πŸ€ͺ Not Person's prototype
console.log(Person.__proto__); / / 😳 Person 's prototype
Copy the code

So the prototype chain “is more like __proto __. __ proto __. __ proto__ rather than a prototype. The prototype. The prototype. It took me years to get it.

So what are the stereotype properties of a function or class? It is __proto__ given to all objects created using this class or function!

function Person(name) {
  this.name = name;
}
Person.prototype.sayHi = function() {
  alert('Hi, I am ' + this.name);
}

var fred = new Person('Fred'); // Sets `fred.__proto__` to `Person.prototype`
Copy the code

And the __proto__ chain is how JavaScript looks up attributes:

fred.sayHi();
// 1. Does fred have a sayHi property? No.
// 2. Does fred.__proto__ have a sayHi property? Yes. Call it!

fred.toString();
// 1. Does fred have a toString property? No.
// 2. Does fred.__proto__ have a toString property? No.
// 3. Does fred.__proto__.__proto__ have a toString property? Yes. Call it!
Copy the code

In practice, you rarely need to touch __proto__ directly from your code unless you are debugging something related to the prototype chain. If you want to provide something on Fred.__ proto__, you should put it on Person.prototype. At least that’s the way it was originally designed.

The __proto__ attribute should not even be exposed by the browser, because the prototype chain is considered an internal concept. But some browsers added __proto__, and eventually it was barely standardized (Object. GetPrototypeOf () was favored).

However, I still find a prototype with an attribute called prototype that doesn’t give you a value (for example, fred.prototype is undefined because Fred isn’t a function), and it really confuses me. Personally, I think this is the biggest reason even experienced developers misunderstand JavaScript prototypes.


This is a long post, huh? I say 80% of us are there. Maintained.

We know that when we say obj. Foo, JavaScript actually looks for foo in obj, obj. __ proto__, obj. __ proto.__ proto__, and so on.

For classes, you won’t be directly exposed to this mechanism, but extensions work well with old prototype chains as well. This is how an instance of our React class accesses methods like setState:

class Greeting extends React.Component {
  render() {
    return <p>Hello</p>; }}let c = new Greeting();
console.log(c.__proto__); // Greeting.prototype
console.log(c.__proto__.__proto__); // React.Component.prototype
console.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Found on c.__proto__ (Greeting.prototype)
c.setState();    // Found on c.__proto__.__proto__ (React.Component.prototype)
c.toString();    // Found on c.__proto__.__proto__.__proto__ (Object.prototype)
Copy the code

In other words, when using a class, the instance’s __proto__ chain “mirrors” to the class hierarchy:

// `extends` chainThe Greeting - ponent - > React.ComObject (implicitly)

// `__proto__` chain
newThe Greeting () - > the Greeting. Prototype - > React.Com ponent. Prototype - >Object.prototype
Copy the code

Since the __proto__ chain reflects the class hierarchy, we can check if Greeting extends react.ponent by starting with greeting. prototype and then following its __proto__ chain:

// `__proto__` chain
newThe Greeting () - > the Greeting. The prototype// πŸ•΅οΈ We start here- > React.Com ponent. Prototype/ / βœ… Found it!
      β†’ Object.prototype
Copy the code

Conveniently, X Instanceof Y does complete this search. It follows the X. __ proto__ chain where it looks for y.prototype.

Typically, it is used to determine if something is an instance of a class:

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (πŸ•΅οΈβ€ We start here)
//.__proto__ β†’ Greeting. Prototype (βœ… Found it!)
/ /. __proto__ - > React.Com ponent. Prototype
/ /. __proto__ - > Object. The prototype

console.log(greeting instanceof React.Component); // true
// greeting (πŸ•΅οΈβ€ We start here)
/ /. __proto__ - the Greeting. The prototype
/ /. __proto__ - > React.Com ponent. Prototype (βœ… Found it!)
/ /. __proto__ - > Object. The prototype

console.log(greeting instanceof Object); // true
// greeting (πŸ•΅οΈβ€ We start here)
/ /. __proto__ - the Greeting. The prototype
/ /. __proto__ - > React.Com ponent. Prototype
//.__proto__ β†’ object. prototype (βœ… Found it!)

console.log(greeting instanceof Banana); // false
// greeting (πŸ•΅οΈβ€ We start here)
/ /. __proto__ - the Greeting. The prototype
/ /. __proto__ - > React.Com ponent. Prototype
//.__proto__ β†’ object. prototype (πŸ™…β€ Did not find it!)
Copy the code

But it also works to determine if one class extends another:

console.log(Greeting.prototype instanceof React.Component);
// greeting
//.__proto__ β†’ Greeting. Prototype (πŸ•΅οΈβ€ We start here)
/ /. __proto__ - > React.Com ponent. Prototype (βœ… Found it!)
/ /. __proto__ - > Object. The prototype
Copy the code

That check is how we determine whether something is a React component class or a regular function.


But that’s not what React does. 😳

One caveat to the Instanceof solution is that it doesn’t work when there are multiple copies of React on the page, and the component we’re examining inherits react.componentfrom another Copy of React. Mixing multiple copies of React in a project is bad for several reasons, but historically we’ve tried to avoid problems as much as possible. (Using Hooks, we might need to force deduplication.)

Another possible heuristic might be to check whether rendering methods exist on the prototype. However, it was not clear at the time how the component API would evolve. Each check has a cost, so we don’t want to add more than one. This also doesn’t work if render is defined as an instance method (such as using class attribute syntax).

React therefore adds a special flag to the base component. React checks for the presence of the flag, which is how it knows if something is a React component class or a function.

The original logo is based on the React.Component class:

// Inside React
class Component {}
Component.isReactClass = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); / / βœ… Yes
Copy the code

However, some of the class implementations we want to locate do not copy static attributes (or set a nonstandard __proto__), so flags are lost.

This is a sign of the React to the move to React.Com ponent. The cause of the prototype:

// Inside React
class Component {}
Component.prototype.isReactComponent = {};

// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); / / βœ… Yes
Copy the code

That’s really what it’s all about.

You may wonder why it is an object and not just a Boolean value. It is not important in practice, but earlier versions of Jest (before Jest was Goodℒ️) enabled automatic locking by default. The generated Mocks omit the original attributes, breaking the check. Thank you Jest.

The isReactComponent check is used in React today.

If you do not extend react.componentreact will not find the isReactComponent on the prototype and will not treat the component as a class. Now you know why the most popular answer is: Cannot call a class as a function The wrong answer is to add extends react.ponent. Finally, add a warning when the prototype. The warning is render, but the prototype. IsReactComponent does not exist.


The actual solution was pretty simple, but I went on to explain why React ended up with this solution and what the alternatives were.

In my experience, this is usually the case with library apis. To make an API easy to use, there is often a need to consider language semantics (possibly, for multiple languages, including future directions), runtime performance, ergonomics with and without compile-time steps, the state of the ecosystem and packaging solution, early warning, and many other things. The end result may not always be the most elegant, but it has to be practical.

If the final API succeeds, its users never have to think about this process. Instead, they can focus on building applications.

But if you’re also curious…… It’s nice to know how it works.