• How Does React Tell a Class from a Function?
  • By Dan Abramov
  • The Nuggets translation Project
  • Permanent link to this article: github.com/xitu/gold-m…
  • Translator: Washington Hua
  • Proofread by: Nanjingboy, Sunui

Let’s look at the new 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 the State feature)

When you render a
component, you don’t need to care how it is defined:

// Class or function -- doesn't matter.Copy the code

But React itself cares about the difference!

If Greeting is a function, React needs to call it.

// Your codefunction Greeting() {
  return<p>Hello</p>; } // React internal const result = Greeting(props); // <p>Hello</p>Copy the code

But if Greeting is a class, React needs to instantiate it with the new operator and then call the Render method that just generated the instance:

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

In either case, the goal of React is to fetch the rendered node (in this case,

Hello

). But the steps depend on how Greeting is defined.

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

As I mentioned in my last blog post, you don’t need to know this to use React effectively. I didn’t know that for years. Please don’t make this an interview question. In fact, this blog post is more about JavaScript than React.

This blog post is for readers who are curious about exactly how React works. Are you one of those people? Let’s talk about it in more depth.

It’s going to be a long ride. Buckle up. This article doesn’t have much information about React itself, but we’ll cover itnew,this,class, arrow function,prototype,__proto__,instanceofAnd how these things work together in JavaScript. Fortunately, you don’t have to think about this all the time when you use React, unless you’re implementing React…

(If you really want to know, flip to the bottom.)


First, we need to understand why it is important to treat functions and classes separately. Notice how we call a class using the new operator:

// 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, JavaScript didn’t have classes. However, you can use ordinary functions to simulate. In particular, you can use any function as a constructor of a class by preending the function call with the new operator:

// Just a functionfunction Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); / / ✅ Person {name:'Fred'}
var george = Person('George'); // 🔴 is uselessCopy the code

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

If you call Person(‘Fred’) without new, this will point to something global and useless (like window or undefined), so our code will crash or do something stupid like setting window.name.

By adding new before the call, we say: “Hey JavaScript, I know Person is just a function, but let’s pretend it’s a constructor. Create a {} object and point this in Person to that object so I can set something like this.name and return that object to me.”

That’s what the new operator does.

var fred = new Person('Fred'); // The object equivalent to 'this' in' Person 'Copy the code

The new operator also puts on the Fred object what we put 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 simulated classes before JavaScript directly supported them.


New has been around in JavaScript for a long time, but classes are still relatively recent, allowing us to refactor our previous code to make it more like we intended:

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

Capturing the developer’s intent is an important part of language and API design.

If you write a function, JavaScript has no way of determining whether it should be called like alert() or treated as a constructor like new Person(). Forgetting to specify new for functions like Person can lead to confusing behavior.

Class syntax allows us to say, “This is not just a function — this is a class and it has a constructor.” If you forget to add new when you call it, JavaScript will report an error:

let fred = new Person('Fred'); // ✅ if Person is a function: valid // ✅ if Person is a class: still validlet george = Person('George'); // We forgot to use 'new' // 😳 if Person is a method that looks like a constructor: confusing behavior // 🔴 if Person is a class: immediate failureCopy the code

This will help us catch errors early without the implicit error of this.name being treated as window.name instead of george.name.

However, this means that React needs to be called with new in front of all classes and cannot 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 simply do this: const instance = Counter(props);Copy the code

That means trouble.


Before we look at how React handles this issue, it’s important to remember that most React users use compilers like Babel to compile classes and other modern features to run on older browsers. So we need to consider compilers in our design.

In earlier versions of Babel, classes could be called without new. But this has been fixed — by generating extra code.

functionPerson(name) {// Simplifies the output of Babel slightly:if(! (this instanceof Person)) { throw new TypeError("Cannot call a class as a function"); } // Our code: this.name = name; } new Person('Fred'); / / ✅ OK Person ('George'); // 🔴 cannot call a class as a functionCopy the code

You’ve probably seen code like this in packages you’ve built, and that’s what the _classCallCheck functions do. (You can reduce the build package size by turning off the check by enabling “loose Mode,” but this might complicate when you finally switch to the real native class.)


At this point, you should have a rough idea of the difference between calling with or without new:

new Person() Person()
class thisIs aPersonThe instance 🔴 TypeError
function thisIs aPersonThe instance 😳 thiswindowundefined

That’s why it’s important that React calls your components correctly. If your component is defined as a class, React needs to call it using new

So can React check to see if something is a class?

It’s not that easy! Even if we were able to distinguish between categories and functions in JavaScript, it wouldn’t work for classes handled by tools like Babel. To the browser, they are just different functions. This is React’s misfortune.


Okay, can’t React just call new every time? Unfortunately, this approach doesn’t always work.

For regular functions, a call to new gives them an object instance of this. This is fine for functions that are used as constructors (such as the Person we mentioned earlier), but can be confusing for function components:

function Greeting() {// We do not expect 'this' to represent instances of any type herereturn <p>Hello</p>;
}
Copy the code

That is tolerable for now, and there are two other reasons to kill the idea.


The first reason why it is useless to always use new is that for native arrow functions (not those compiled by Babel), calling new throws an error:

const Greeting = () => <p>Hello</p>; new Greeting(); // 🔴 Greeting is not a constructorCopy the code

This behavior is intentional following the design of the arrow function. A side effect of the arrow function is that it does not have its own this value — this resolves from the nearest normal function:

class Friends extends React.Component {
  render() {
    const friends = this.props.friends;
    returnFriend.map (friend => < friend // 'this' parsing from' render 'method size={this.props. Size} name={friend.name} key={friend.id} />); }}Copy the code

OK, so the ** arrow function doesn’t have its own this. ** But that means it’s completely useless as a constructor!

Const Person = (name) => {// 🔴 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 and it’s best to tell you sooner. This is similar to how JavaScript doesn’t let you call a class without adding new.

That’s great, but it’s also thwarting our plans. React can’t simply use new on everything because it would break the arrow function! We can check arrow functions using the fact that they don’t have prototype and don’t use new on them:

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

But this is useless for functions compiled by Babel. That may not be a big deal, but there is another reason why this path will not bear fruit.


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

This, again, has to do with the weird design of the new operator. As we saw earlier, new tells the JavaScript engine to create an object, call it this inside the function, and give it to us as the result of new.

However, JavaScript also allows a function called with new to return another object to override the return value of new. Perhaps this is useful when we reuse components using such “object pooling patterns” :

// create a lazy variable zeroVector = null;function Vector(x, y) {
  if (x === 0 && y === 0) {
    if(zeroVector ! == null) {// reuse the same instancereturn 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 return value of a function is not an object, it is completely ignored by new. If you return a string or a number, it is as if there is no return at all.

function Answer() {
  return42. } Answer(); // ✅ 42 new Answer(); / / 😳 Answer {}Copy the code

When calling a function with new, there is no way to read the return value of a primitive type (such as a number or string). So if React always uses new, there’s no way to add support for components that return strings!

This is unacceptable, so we have to compromise.


What have we learned so far? React calls to classes (including Babel output) require new, but calls to regular or arrow functions (including Babel output) do not, and there is no reliable way to tell the difference.

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

When you define a component as a class, you’ll probably want to extend react.ponent to get built-in methods, such as this.setstate (). Instead of trying to detect all classes, can we detect only the descendants of React.component?

Here’s what the React does.


Perhaps the most language-appropriate way to check if Greeting is a 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 the prototype chain. Every object in JavaScript has a “prototype.” When we write Fred.sayhi () and Fred doesn’t have the sayHi attribute, we try to find the sayHi attribute in Fred’s prototype. If we don’t find it here, we’ll look for the next prototype in the chain — Fred’s prototype, and so on.

Inexplicably, 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); // 🤪 is not the prototype of Person console.log(person.__proto__); // 😳 Person prototypeCopy the code

So “prototype chain” is more like __proto__. __proto__. __proto__ rather than a prototype. The prototype. The prototype, it took me years to understand this.

What are the prototype properties of functions and classes? Is the __proto__ of all objects generated by calling that class or function with new!

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

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

The __proto__ chain is what JavaScript uses to find attributes:

fred.sayHi(); // 1. Does Fred have sayHi? Not at all. // 2. Fred.__proto__ has the sayHi attribute? Yes, call it! fred.toString(); // 1. Does Fred have toString? Not at all. // does Fred.__proto__ have toString attributes? Not at all. // 3. Fred.__proto__.__proto__ has toString attributes? Yes, call it!Copy the code

In practice, you should almost never need to touch __proto__ directly in code unless you are debugging problems related to the prototype chain. If you want something to work on Fred.__proto__, you should put it in Person.prototype, at least that’s how it was originally designed.

The __proto__ attribute should not even be exposed by browsers in the first place, as the prototype chain should be treated as an internal concept, yet some browsers added __proto__ and eventually managed to standardize (but have since been deprecated and recommend object.getPrototypeof ()).

I’m still confused that a property called “prototype” doesn’t give me a value for “prototype” (for example, fred.prototype is undefined because Fred is not a function). Personally, I think this is the biggest reason that JavaScript prototype chains are often misunderstood by even experienced developers.


It’s a long blog, isn’t it? It’s up to 80%. Hang in there.

We know that when we say obj.foo, JavaScript actually looks for foo all the way down obj, obj.__proto__, obj.__proto__.__proto__, and so on.

You don’t face this mechanism directly when using classes, but the principle of extends is still based on the old but effective prototype chain mechanism. That’s why our React class instance can access methods like setState:

class Greeting extends React.Component {
  render() {
    return<p>Hello</p>; }}letc = 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(); // find c.setstate () on c.__proto__ (greeting. prototype); / / in c. __proto__. __proto__ (React.Com ponent. Prototype) found on the c.t oString (); / / in c. __proto__. __proto__. __proto__ (Object. The prototype)Copy the code

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

// 'extends' chain new Greeting() → new __proto__ () → new Greeting. Prototype → React.Com ponent. Prototype - > Object. The prototypeCopy the code

Article 2 the chain.


Since the __proto__ chain mirrors the class hierarchy, we can check if a Greeting extends react.component. we start with greeting. prototype and work our way up the __proto__ chain:

/ / ` __proto__ ` chain new Greeting () - > the Greeting. Prototype / / 🕵 ️ ponent. We start from here - React.Com prototype / / ✅ find! - > Object. The prototypeCopy the code

Conveniently, x Instanceof Y does just that sort of search. It follows the X.__proto__ chain to see if y.trototype is there.

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

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true//.__proto__ → greeting. Prototype (✅ found!) / /. __proto__ - > React.Com ponent. Prototype / /. __proto__ - > Object. The prototype console. The log (the greeting instanceof React.Component); //true/ / the greeting (🕵 ️ we start from here). / / __proto__ - the greeting. Prototype / /. __proto__ - > React.Com ponent. Prototype (✅ found!) //.__proto__ → Object.prototype console.log(greeting instanceof Object); //true/ / the greeting (🕵 ️ we start from here). / / __proto__ - the greeting. Prototype / /. __proto__ - > React.Com ponent. Prototype / /. __proto__ - > Object.prototype (✅ found!) console.log(greeting instanceof Banana); //false/ / the greeting (🕵 ️ we start from here). / / __proto__ - the greeting. Prototype / /. __proto__ - > React.Com ponent. Prototype / /. __proto__ - > Object.prototype (🙅 not found!)Copy the code

But it is valid to determine whether one class extends another

console.log(Greeting.prototype instanceof React.Component); / / the greeting / /. __proto__ - the greeting. Prototype (🕵 ️ we start from here). / / __proto__ - > React.Com ponent. Prototype (✅ found!) / /. __proto__ - > Object. The prototypeCopy the code

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


React doesn’t do that 😳

One caveat about the Instanceof solution is that this method does not work when there are multiple copies of React on the page and the component we want to check inherits react.componentfrom another Copy of React. Mixing multiple React replicas in a project is bad for a number of reasons, but historically we try to avoid problems as much as possible. (With Hooks, we may have to force avoid repetition)

Another heuristic might be to examine the Render method on the prototype chain. However, it was uncertain how the component’s API would evolve. Every inspection has a cost, so we don’t want to add any more. If Render is defined as an instance method, such as using class attribute syntax, this method will also be invalidated.

React therefore adds a special tag to the base class. React checks for this flag to know if something is a React component class.

Originally this tag was on the base class react.componentitself:

// React internal class Component {} component. isReactClass = {}; // We can check it like this. Class Greeting extends Component {} console.log(greeting. isReactClass); / / ✅ yesCopy the code

However, some of the class implementations we want to target don’t copy static properties (or set a nonstandard __proto__), and the markup is lost as a result.

This is why the React to this tag ponent moved to React.Com. The prototype:

/ / the React within class Component {} Component. The prototype. IsReactComponent = {}; / / we can check it in the class like this Greeting extends Component. {} the console log (the Greeting. Prototype. IsReactComponent); / / ✅ yesCopy the code

Seriously that’s all there is to it.

You may wonder why it is an object and not a Boolean. This is not important in practice, but earlier versions of Jest (before Jest was commercialized) started automating by default, generating simulation data that omitted primitive type attributes and broke checks. Thanks, Jest.

React is checked with the isReactComponent to this day.

If you don’t extend the react.component.react won’t find the isReactComponent on the prototype, so it won’t treat the component as a class. Now you know why the highest vote-getter answer to the Cannot Call a class as a function error is to add extends react.ponent. Finally, we added a warning, as the prototype, render exist. But the prototype will warn isReactComponent does not exist.


You might think this story is a bit clickbait. The actual solution was really simple, but I spent a lot of time explaining why React chose it and what the alternatives were.

In my experience, this is often the case when designing a library’S API. For an API to be easy to use, you often need to consider semantematization (for multiple languages, if possible, including future directions), runtime performance, engineering efficiency with or without compile-time steps, ecological state and packaging schemes, early warnings, and many other issues. The end result may not always be the most elegant, but it must be usable.

If the final API is successful, its users will never have to think about this process. They just need to focus on building the app.

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

If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.


The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.