React components can be defined as either functions or classes. For example, a simple Greeting component might look like this when defined as a function:
function Greeting() {
return <p>Hello</p>;
}
Alternatively, the same component can be implemented as a class:
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
Historically, classes were the only way to access features like state in React, until the introduction of Hooks.
From a user’s perspective, rendering a <Greeting /> component remains the same, regardless of its underlying definition:
// Class or function — whatever.
<Greeting />
However, React itself must distinguish between these two types of components. If Greeting is a function, React directly invokes it:
// Your code
function Greeting() {
return <p>Hello</p>;
}
// Inside React
const result = Greeting(props); // <p>Hello</p>
But if Greeting is a class, React first instantiates it using the new operator, and then calls its render method:
// Your code
class Greeting extends React.Component {
render() {
return <p>Hello</p>;
}
}
// Inside React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>
In both scenarios, React’s objective is to obtain the rendered output, such as <p>Hello</p>. The specific steps, however, depend entirely on whether Greeting is a function or a class.
How Does React Differentiate Between Classes and Functions?
Understanding this distinction is not essential for everyday React development. Many developers work with React for years without needing to know these internal mechanisms. This discussion is primarily for those curious about the underlying JavaScript principles that enable React’s functionality.
This exploration will cover various JavaScript concepts, including the new operator, this context, classes, arrow functions, prototypes, __proto__, and instanceof. While these concepts are fundamental to JavaScript, React abstracts much of this complexity for its users.
The Role of the new Operator
The distinction between functions and classes is crucial due to JavaScript’s new operator. When a class is invoked, it typically uses new:
// 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>
To understand this, consider JavaScript’s evolution. Before explicit class syntax, developers used functions as constructors. Any function could act as a constructor by prefixing its call with new:
// Just a function
function Person(name) {
this.name = name;
}
var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Won’t work
Calling Person('Fred') without new would result in this referring to the global object (e.g., window or undefined), potentially causing errors or unintended side effects like setting window.name.
The new operator instructs JavaScript to: create a new empty object, set that object as the this context within the constructor function, allow properties like this.name to be assigned to it, and finally return this newly created object.
var fred = new Person('Fred'); // Same object as `this` inside `Person`
Additionally, properties defined on Person.prototype become accessible on the fred object:
function Person(name) {
this.name = name;
}
Person.prototype.sayHi = function() {
alert('Hi, I am ' + this.name);
}
var fred = new Person('Fred');
fred.sayHi();
This mechanism was JavaScript’s way of simulating classes before their native introduction.
Modern JavaScript classes, introduced later, provide a clearer syntax for this pattern:
class Person {
constructor(name) {
this.name = name;
}
sayHi() {
alert('Hi, I am ' + this.name);
}
}
let fred = new Person('Fred');
fred.sayHi();
Class syntax explicitly communicates developer intent. A function’s purpose (regular call vs. constructor) isn’t always obvious. Forgetting new with a constructor function could lead to subtle bugs. Classes, however, enforce this. Attempting to call a class without new results in a TypeError:
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
This early error detection is beneficial, preventing harder-to-debug issues. Consequently, React must use new when invoking a class component, as a direct function call would lead to an error:
class Counter extends React.Component {
render() {
return <p>Hello</p>;
}
}
// 🔴 React can't just do this:
const instance = Counter(props);
This requirement presents a challenge for React.
Many React applications use compilers like Babel to transpile modern JavaScript features, including classes, for broader browser compatibility. Early versions of Babel allowed classes to be called without new, but this was later corrected by generating additional runtime checks, such as _classCallCheck functions, to ensure proper class invocation:
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'); // 🔴 Cannot call a class as a function
This table summarizes the behavior:
new Person()with a class: ✅ creates aPersoninstance.Person()with a class: 🔴TypeError.new Person()with a function: ✅ creates aPersoninstance.Person()with a function: 😳thisiswindoworundefined.
Therefore, React must correctly identify and invoke class components with new.
Challenges in Distinguishing Components
Directly checking if something is a JavaScript class is complicated, especially because tools like Babel transpile classes into functions for compatibility. From a browser’s perspective, a Babel-compiled class often appears as a regular function, making a simple class check unreliable for React.
An alternative might be for React to always use the new operator when invoking components. While this would work for class components, and even for regular functions (though it would set this to an object instance, which is unusual for function components):
function Greeting() {
// We wouldn’t expect `this` to be any kind of instance here
return <p>Hello</p>;
}
This approach has significant drawbacks.
Arrow Functions and new
Native arrow functions cannot be called with new; doing so results in a TypeError:
const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is not a constructor
This behavior is by design. Arrow functions do not have their own this context; instead, they inherit this from their enclosing scope:
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}
/>
);
}
}
Because arrow functions lack their own this, they are unsuitable as constructors:
const Person = (name) => {
// 🔴 This wouldn’t make sense!
this.name = name;
}
JavaScript intentionally prevents calling arrow functions with new to avoid misuse. This means React cannot universally apply new to all components, as it would break arrow function components. While detecting arrow functions by their lack of a prototype property might seem like a solution:
(() => {}).prototype // undefined
(function() {}).prototype // {constructor: f}
This method is unreliable for functions processed by Babel, which might alter their characteristics. Moreover, another critical issue prevents this universal new approach.
Returning Primitive Types
The new operator’s design also impacts components that return primitive values like strings or numbers:
function Greeting() {
return 'Hello';
}
Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}
When a function is called with new, JavaScript typically creates and returns a new object. However, if the function explicitly returns an object, that object overrides the default return value:
// 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
Crucially, if a function called with new returns a primitive value (like a string or number), JavaScript completely ignores it, and the operator still returns the initially created object:
function Answer() {
return 42;
}
Answer(); // ✅ 42
new Answer(); // 😳 Answer {}
This behavior means that if React always used new, it would be impossible to support components that return primitive types directly, as their return values would be discarded. This limitation is unacceptable for React’s flexibility.
Detecting React.Component Descendants
Given the difficulties in reliably distinguishing all classes from functions, React focuses on a more specific problem: identifying components that extend React.Component. This is because class components in React typically inherit from React.Component to gain access to methods like this.setState().
A potential method to check if a component like Greeting is a React class component would involve testing Greeting.prototype instanceof React.Component:
class A {}
class B extends A {}
console.log(B.prototype instanceof A); // true
To understand why this works, it’s necessary to delve into JavaScript prototypes.
Understanding JavaScript Prototypes
Every object in JavaScript can have a “prototype.” When attempting to access a property (e.g., fred.sayHi()) that an object (fred) does not directly possess, JavaScript searches for that property on fred‘s prototype. If not found there, the search continues up the “prototype chain” (fred‘s prototype’s prototype, and so on).
A common point of confusion is that the prototype property of a class or function does not directly point to the prototype of that specific value. Instead, the actual prototype chain is traversed via __proto__, like __proto__.__proto__.__proto__.
function Person() {}
console.log(Person.prototype); // 🤪 Not Person's prototype
console.log(Person.__proto__); // 😳 Person's prototype
The prototype property on a function or class actually defines the __proto__ property for all objects created using new with that function or class:
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`
This __proto__ chain is how JavaScript resolves property lookups:
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!
While __proto__ is an internal mechanism and generally not directly manipulated in application code (Object.getPrototypeOf() is the standard way to access it), it’s fundamental to how inheritance works. The prototype property of a constructor function is where properties and methods intended for instances are placed.
When using classes with the extends keyword, the prototype chain mechanism is still at play. This is how a React class instance inherits 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)
Essentially, the __proto__ chain of an instance reflects its class hierarchy:
// `extends` chain
Greeting
→ React.Component
→ Object (implicitly)
// `__proto__` chain
new Greeting()
→ Greeting.prototype
→ React.Component.prototype
→ Object.prototype
Given that the __proto__ chain mirrors the class hierarchy, it’s possible to determine if a component like Greeting extends React.Component by traversing Greeting.prototype‘s __proto__ chain:
// `__proto__` chain
new Greeting()
→ Greeting.prototype // 🕵️ We start here
→ React.Component.prototype // ✅ Found it!
→ Object.prototype
The instanceof operator performs precisely this kind of check. It traverses the __proto__ chain of its left-hand operand, searching for the prototype property of its right-hand operand.
Typically, instanceof is used to check if an object is an instance of a particular class:
let greeting = new Greeting();
console.log(greeting instanceof Greeting); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype (✅ Found it!)
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype
console.log(greeting instanceof React.Component); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
console.log(greeting instanceof Object); // true
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (✅ Found it!)
console.log(greeting instanceof Banana); // false
// greeting (🕵️ We start here)
// .__proto__ → Greeting.prototype
// .__proto__ → React.Component.prototype
// .__proto__ → Object.prototype (🙅 Did not find it!)
However, it can also effectively determine if one class extends another:
console.log(Greeting.prototype instanceof React.Component);
// greeting
// .__proto__ → Greeting.prototype (🕵️ We start here)
// .__proto__ → React.Component.prototype (✅ Found it!)
// .__proto__ → Object.prototype
This instanceof check could theoretically serve as a mechanism for React to differentiate between class components and regular functions.
React’s Actual Solution: The isReactComponent Flag
Despite the theoretical viability of instanceof, React does not use it for this purpose. One reason is the potential for issues with multiple copies of React on a single page, where a component might inherit from a different React instance’s React.Component, leading to false negatives. While multiple React instances are generally discouraged, React aims to avoid such problems where possible.
Another heuristic considered was checking for the presence of a render method on the prototype. However, this approach had limitations, including uncertainty about future API evolution and the inability to detect render methods defined as class properties.
Instead, React implemented a more direct solution: a special flag added to its base component. React checks for the presence of this flag to identify a React class component.
Initially, this flag was a static property on the React.Component class itself:
// Inside React
class Component {}
Component.isReactClass = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Yes
However, certain JavaScript class implementations did not correctly copy static properties or set the non-standard __proto__, causing the flag to be lost during inheritance. To address this, React moved the flag to React.Component.prototype:
// Inside React
class Component {}
Component.prototype.isReactComponent = {};
// We can check it like this
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Yes
This simple check is how React identifies class components. The flag is an object rather than a boolean due to historical issues with early versions of Jest’s automocking, which would omit primitive properties, thereby breaking the check.
The isReactComponent check remains in use within React today. If a component does not extend React.Component, React will not find this flag on its prototype and will not treat it as a class. This explains why adding extends React.Component is a common solution for “Cannot call a class as a function” errors. Furthermore, React now issues a warning if a component’s prototype has a render method but lacks the isReactComponent flag.
Conclusion
The actual mechanism React uses to distinguish class components from function components is surprisingly straightforward: a simple flag on the prototype. However, the journey to this solution involved navigating numerous complexities of JavaScript’s object model, the new operator, arrow functions, and the practical considerations of a vast ecosystem including transpilers and testing frameworks.
This process highlights a common theme in library and API design. Achieving a user-friendly API often requires deep consideration of language semantics, runtime performance, developer ergonomics, build processes, and ecosystem compatibility. The resulting solution may not always be the most elegant from a purely academic perspective, but it must be robust and practical.
Ultimately, a successful API allows its users to focus on application development without needing to understand these intricate underlying details. For those with a deeper curiosity, however, exploring these mechanisms provides valuable insight into how powerful tools like React are built.

