Overreacted

Hoe onderscheidt React een class van een functie?

2 december 2018 • ☕️☕️☕️☕️ 18 min read

Neem deze Greeting component, gedefinieerd als functie:

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

Als we deze als class zouden definiëren zou dit geen probleem zijn voor React:

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

(Dit was tot kort geleden bijvoorbeeld de enige manier om ‘state’ te gebruiken.)

Nu is het begrijpelijk dat het voor jou niet veel uitmaakt hoe je <Greeting /> definieert. Zolang het maar werkt:

// Class of functie — wat maakt het uit.
<Greeting />

Echter, voor React maakt het wel uit!

Als Greeting namelijk een functie is, moet React deze eerst aanroepen voordat het goed werkt:

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

// In React
const result = Greeting(props); // <p>Hello</p>

Echter, als Greeting een class is moet React deze eerst initialiseren met de new operator om daarna de render method aan te roepen binnen de zojuist gecreëerde instantie van Greeting:

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

// In React
const instance = new Greeting(props); // Greeting {}
const result = instance.render(); // <p>Hello</p>

In beide gevallen heeft React als doel om de node te renderen (in dit voorbeeld <p>Hello</p>). Echter, de manier waarop React daarvoor te werk moet gaan is afhankelijk van de manier waarop Greeting is gedefinieerd.

Dus, hoe weet React of iets een class of een functie is?

Zoals ik in mijn vorige blogpost al vertelde: hoe React dit doet is geen kennis die je nodig hebt om goed te kunnen werken met React. Zelf wist ik dit ook jarenlang niet. Ik zou het dan ook zeker niet aan iemand vragen tijdens een interview. Deze blogpost gaat om heel eerlijk te zijn eigenlijk ook meer over JavaScript dan over React.

Dus, ben jij een nieuwsgierige lezer die wil weten waarom React op een bepaalde manier werkt? Laten we er dan snel in duiken.

Bereid je voor… Dit is een lang verhaal waarbij ik het vooral ga hebben over JavaScript en niet over React. Ik bespreek wel een aantal aspecten rondom new, this, class, arrow functions, prototype, __proto__, instanceof en de manier waarop deze samenwerken in JavaScript. Gelukkig hoef je niet veel na te denken over die dingen als je React gebruikt. Echter, als je React implementeert…

(Als je gewoon wil weten hoe React het verschil tussen een class en functie weet kan je ook gewoon naar het einde scrollen.)


Om te beginnen moeten we begrijpen waarom het belangrijk is om het verschil tussen functies en classes te weten. Beide worden namelijk anders behandeld. Zie hier hoe we de new operator gebruiken als we een class aanroepen:

// Als Greeting een functie is
const result = Greeting(props); // <p>Hello</p>

// Als Greeting een class is
const instance = new Greeting(props); // Greeting {}const result = instance.render(); // <p>Hello</p>

Laten we in een notendop kijken wat de new operator doet in JavaScript.


Zelfs toen JavaScript vroeger geen classes had kon je een vergelijkbaar patroon als die van een class gebruiken door functies in te zetten. Heel concreet: je kan iedere functie dezelfde rol geven als een class constructor door new te plaatsen voor het aanroepen:

// Een normale functie
function Person(name) {
  this.name = name;
}

var fred = new Person('Fred'); // ✅ Person {name: 'Fred'}
var george = Person('George'); // 🔴 Werkt niet

Zelfs nu dat JavaScript classes heeft werkt bovenstaande code nog steeds! Probeer het maar eens in DevTools.

Als je Person(‘Fred’) aanroept zonder new, verwijst this naar iets globaals en onhandigs (bijvoorbeeld window of undefined). Onze code zou dus kunnen crashen of iets raars doen zoals window.name creëren.

Door new te gebruiken zeggen we eigenlijk: “Hey JavaScript, ik weet dat Person gewoon een functie is. Maar laten deze gebruiken alsof het een class constructor is. Maak een {} object en verwijs this binnen de Person functie naar dat {} object zodat ik dingen zoals this.name kan toewijzen. Geef mij daarna dat object weer terug.”

En dat is wat de new operator doet.

var fred = new Person('Fred'); // Hetzelfde object als ‘this’ in ‘Person’

De new operator zorgt er ook voor dat alles wat we op Person.prototype zetten beschikbaar is in het 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();

Dit is hoe mensen vroeger classes nabootsten voordat ze officieel werden toegevoegd aan JavaScript.


Waar new al een tijdje te gebruiken is in JavaScript, zijn classes nieuwer. Classes maken het ons mogelijk om de code hierboven te herschrijven op een leesbaardere manier die duidelijker maakt wat we ermee willen bereiken:

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

let fred = new Person('Fred');
fred.sayHi();

Die duidelijkheid over wat de developer ergens mee wil bereiken is enorm belangrijk. Het is dus belangrijk om hier rekening mee te houden bij zowel het ontwerpen van programmeertalen als API’s.

Als je een functie schrijft kan JavaScript niet zelf inschatten of deze moet worden aangeroepen met alert() of dat het een constructor moet zijn zoals new Person(). new vergeten bij het aanroepen van Person kan voor verwarrende resultaten zorgen.

Dankzij de syntax van class is het mogelijk te zeggen: “Dit is niet zomaar een functie - het is een class en het heeft een constructor”. Als je new vergeet te gebruiken wanneer je deze aanroept geeft JavaScript een error:

let fred = new Person('Fred');
// ✅ Als Person een functie is: werkt prima
// ✅ Als Person een class is: werkt ook prima

let george = Person('George'); // We zijn ‘new’ vergeten
// 😳 Als Person een constructor-achtige functie is: verwarrend gedrag
// 🔴 Als Person een class is: geeft direct een error

Dit helpt ons om snel fouten te vinden. In plaats van dat we moeten wachten totdat er een rare bug tevoorschijn komt. Zoals this.name die geïmplementeerd wordt als window.name in plaats van george.name.

Dit betekent echter wel dat React new moet gebruiken voordat een class aangeroepen wordt. Een class kan niet aangeroepen worden als een normale functie, sinds JavaScript dit als een error zou behandelen.

class Counter extends React.Component {
  render() {
    return <p>Hello</p>;
  }
}

// 🔴 Dit is in React niet zomaar mogelijk:
const instance = Counter(props);

Dit vraagt om problemen.


Voordat we gaan kijken hoe React dit oplost is het belangrijk om te realiseren dat de meeste gebruikers van React compilers zoals Babel gebruiken om moderne functionaliteiten zoals classes te kunnen gebruiken in oudere browsers. Dus we moeten rekening houden met compilers in het ontwerp van React.

In oudere versies van Babel konden classes aangeroepen worden zonder new. Dit is echter opgelost - door middel van wat extra code.

function Person(name) {
  // Een versimpelde versie van de Babel output:
  if (!(this instanceof Person)) {
    throw new TypeError("Cannot call a class as a function");
  }
  // Onze code:
  this.name = name;
}

new Person('Fred'); // ✅ Okay
Person('George'); // 🔴 Cannot call a class as a function.

Het kan zijn dat je dit soort code hebt gezien in je bundle. Dit is wat al die _classCallCheck functies doen. (Je kan de bundle size optimaliseren door gebruik te maken van de ‘loose mode’ waar geen checks in zitten. Maar dit kan het mogelijk wel moeilijker maken om de transitie naar echte native classes te maken).


Oké, hopelijk heb je nu iets meer door wat het verschil is tussen het aanroepen van iets met new en zonder new:

new Person() Person()
class this is een Person instance 🔴 TypeError
function this is een Person instance 😳 this is window of undefined

Dit is nou precies waarom het belangrijk is dat React jouw component correct aanroept. Als je component is gedefinieerd als een class moet React new gebruiken wanneer deze wordt aangeroepen.

Dus, kan React gewoon checken of iets een class is of niet?

Nou, zo makkelijk is dat dus niet. Zelfs als we een class van een functie zouden kunnen onderscheiden in JavaScript zou dit niet werken voor classes die zijn verwerkt door tools zoals Babel. Voor de browser is het namelijk gewoon een functie. Pech voor React.


Oke, dus misschien zou React gewoon new kunnen gebruiken op iedere call? Nou… jammer genoeg zou ook dat niet echt werken.

Normale functies die worden aangeroepen met new krijgen een object instance zoals this. Dat is op zich wenselijk voor functies die als constructor zijn geschreven (zoals Person hierboven) maar het zou verwarrend zijn voor function components:

function Greeting() {
  // We verwachten niet dat `this` hier ook maar enigszins een vorm van een instance is.
  return <p>Hello</p>;
}

Dit hoeft natuurlijk geen probleem te zijn. Toch zijn er nog twee andere redenen waarom het juist wel een probleem is.


De eerste reden waarom new gebruiken niet altijd zou werken is dat native arrow functies aanroepen met new een error geeft (behalve als ze zijn gecompileerd door Babel):

const Greeting = () => <p>Hello</p>;
new Greeting(); // 🔴 Greeting is geen constructor

Dit is express gedaan en heeft te maken met het ontwerp van arrow functies. Een van de grootste voordelen van een arrow functie is dat ze niet hun eigen this value hebben - in plaats daarvan komt this van de dichtstbijzijnde normale functie:

class Friends extends React.Component {
  render() {    const friends = this.props.friends;
    return friends.map(friend =>
      <Friend
        // `this` komt uit de ‘render’ method.        size={this.props.size}        name={friend.name}
        key={friend.id}
      />
    );
  }
}

Oke, dus arrow functies hebben geen beschikking over hun eigen this. Wacht… dat zou betekenen dat ze totaal onbruikbaar zijn als constructors!

const Person = (name) => {
  // 🔴 Dit zou niet logisch zijn
  this.name = name;
}

Daarom maakt JavaScript het onmogelijk om een arrow function aan te roepen met new. Als je dit wel zou doen is de kans dat je een fout maakt toch al aanwezig. Dat kan je dan maar beter zo snel mogelijk weten. Het is een beetje hetzelfde als hoe JavaScript het je niet toelaat om een class aan te roepen zonder new.

Heel leuk, maar het maakt ons plan wel iets moeilijker. React kan niet zomaar new aanroepen op ieder type functie omdat het mis zou gaan bij arrow functies. We zouden kunnen proberen om arrow functies te filteren door te kijken of ze geen prototype hebben en hierdoor besluiten of we wel of niet new kunnen gebruiken:

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

Maar dit zou niet werken voor functies die al zijn compiled door Babel. Misschien niet echt een big deal, maar er is ook nog een andere reden die ervoor zorgt dat deze aanpak niet een goed idee is.


Een andere reden waarom we niet altijd new kunnen gebruiken is omdat het React ervan zou weerhouden om components te ondersteunen die strings of andere primitieve types teruggeven.

function Greeting() {
  return 'Hello';
}

Greeting(); // ✅ 'Hello'
new Greeting(); // 😳 Greeting {}

Dit heeft wederom te maken met de gekkigheden van het ontwerp van de new operator. Zoals we eerder zagen vertelt new de JavaScript engine om een object te maken, deze om te zetten in this binnen de functie en dit object later terug te geven als resultaat van new.

Echter, JavaScript staat het functies die zijn aangeroepen met new ook toe om de return value van new te overschrijven door een ander object terug te geven. Waarschijnlijk omdat dit handig zou zijn voor patterns zoals pooling waarbij we instanties willen hergebruiken:

// Lui gemaaktvar zeroVector = null;
function Vector(x, y) {
  if (x === 0 && y === 0) {
    if (zeroVector !== null) {
      // Gebruikt dezelfde 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

new negeert de return value van een functie ook volledig als het geen object is. Als je een string of number zou teruggeven, lijkt het eigenlijk alsof er in eerste instantie geen return is.

function Answer() {
  return 42;
}

Answer(); // ✅ 42
new Answer(); // 😳 Answer {}

Het is gewoon niet mogelijk om een primitieve return value (zoals een number of string) uit te lezen van een functie als deze wordt aangeroepen met new. Dus als React altijd new zou gebruiken, zou deze geen ondersteuning kunnen bieden voor components die strings teruggeven!

Dat is onacceptabel dus we moeten een tussenweg zien te vinden.


Wat hebben we tot nu toe geleerd? React moet classes (inclusief Babel output) aanroepen met new maar het moet normale functies of arrow functies (inclusief Babel output) aanroepen zonder new. En er is geen betrouwbare manier om deze twee van elkaar te onderscheiden.

Als we een algemeen probleem niet kunnen oplossen, kunnen we dan misschien wel een meer specifiek probleem oplossen?

Als je een component als een class definieert wil je waarschijnlijk React.Component gebruiken voor ingebouwde methoden zoals this.setState(). In plaats van alle classes proberen te detecteren, kunnen we ook gewoon op zoek gaan naar React.Component afstammelingen?

Spoiler: dit is precies wat React doet.


Misschien is de meest idiomatische manier om te checken of Greeting een React component class is door dit te testen: Greeting.prototype instanceof React.Component:

class A {}
class B extends A {}

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

Ik weet wat je nu denkt. Wat is hier zojuist gebeurt?! Om dit uit te leggen moeten we JavaScript prototypes begrijpen.

Misschien ben je bekend met de ‘prototype chain’. Ieder object in JavaScript kan een ‘prototype’ hebben. Wanneer we fred.sayHi() schrijven maar fred heeft geen sayHi property, kijken we naar de sayHi property op fred’s prototype. Als we deze hier niet kunnen vinden kijken we naar de volgende prototype in de schakel - de prototype van fred’s prototype. Ga zo maar door.

Het kan wel verwarrend zijn. Dit komt doordat de prototype property van een class of functie niet verwijst naar de prototype van die value. Geloof me.

function Person() {}

console.log(Person.prototype); // 🤪 Niet Person’s prototype
console.log(Person.__proto__); // 😳 Person’s prototype

De ‘prototype chain’ is meer iets als __proto__.__proto__.__proto__ in plaats van prototype.prototype.prototype Het duurde me jaren om dit te begrijpen.

Maar wat is de prototype property van een functie of een class dan? Het is de __proto__ die wordt meegegeven aan alle objecten die zijn aangeroepen met new op die class of functie!

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

var fred = new Person('Fred'); // Zet ‘fred.__proto__’ naar ‘Person.prototype’

En die __proto__ chain is hoe JavaScript properties opzoekt:

fred.sayHi();
// 1. Heeft fred een sayHi property? Nee.
// 2. Heeft fred.__proto__ een sayHi property? Ja! Roep maar aan!

fred.toString();
// 1. Heeft fred een toString property? Nee.
// 2. Heeft fred.__proto__ een toString property? Nee.
// 3. Heeft fred.__proto__.__proto__ een toString property? Ja! Roep maar aan!

In de praktijk zou je eigenlijk nooit __proto__ hoeven aanraken in je code behalve als je iets aan het debuggen bent dat te maken heeft met de prototype chain. Als je iets beschikbaar wil maken op fred.__proto__ moet je deze eigenlijk zetten op Person.prototype. Althans, dat is hoe het ontwerp in eerste instantie was.

De __proto__ property was niet eens bedoelt om beschikbaar gemaakt te worden door browsers omdat de prototype chain werd gezien als een intern concept. Maar sommige browsers hebben __proto__ toegevoegd en uiteindelijk werd het heel erg gestandaardiseerd. (wel deprecated omdat er een preferentie kwam voor Object.getPrototypeOf()).

Toch blijf ik het verwarrend vinden dat een property die prototype genoemd is, niet de prototype teruggeeft van een value (bijvoorbeeld, fred.prototype is undefined omdat fred geen functie is). Persoonlijk denk ik dat dit een van de grootste redenen is dat zelfs developers met veel ervaring moeite hebben met het begrijpen van JavaScript prototypes.


Dit is een lange post of niet? Ik zou zeggen dat we er voor ongeveer 80% zijn. Houd vol.

We weten dat wanneer we obj.foo zeggen, JavaScript op zoek gaat naar foo binnen obj, obj.__proto__, obj.__proto__.__proto__ enzovoorts.

Met classes krijg je niet direct toegang tot dit mechanisme. extends werkt echter wel bovenop de oude vertrouwde prototype chain. Dat is hoe onze React class instance toegang krijgt tot methods zoals 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.prototypeconsole.log(c.__proto__.__proto__.__proto__); // Object.prototype

c.render();      // Gevonden op c.__proto__ (Greeting.prototype)
c.setState();    // Gevonden op c.__proto__.__proto__ (React.Component.prototype)c.toString();    // Gevonden op c.__proto__.__proto__.__proto__ (Object.prototype)

In andere woorden, als je classes gebruikt, ‘weerspiegelt’ de instantie de __proto__ chain van de class hierarchie:

// `extends` chain
Greeting
  → React.Component
    → Object (implicitly)

// `__proto__` chain
new Greeting()Greeting.prototype
    → React.Component.prototype
      → Object.prototype

2 Chainz.


Sinds de __proto__ chain de class hierarchie weerspiegelt kunnen we checken of Greeting de React.Component extend door te starten met Greeting.prototype en dan de __proto__ chain te volgen:

// `__proto__` chain
new Greeting()
Greeting.prototype // 🕵️ We beginnen hier    → React.Component.prototype // ✅ Gevonden!Object.prototype

Handig genoeg doet x instanceof Y precies hetzelfde. Het volgt de x.__proto__ chain om daar naar Y.prototype te zoeken.

Normaal gesproken wordt het gebruikt om te kijken of iets een instance van een class is:

let greeting = new Greeting();

console.log(greeting instanceof Greeting); // true
// greeting (🕵️‍ We beginnen hier)
//   .__proto__ → Greeting.prototype (✅ Gevonden!)
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype

console.log(greeting instanceof React.Component); // true
// greeting (🕵️‍ We beginnen hier)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype (✅ Gevonden!)
//       .__proto__ → Object.prototype

console.log(greeting instanceof Object); // true
// greeting (🕵️‍ We beginnen hier!)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (✅ Gevonden!)

console.log(greeting instanceof Banana); // false
// greeting (🕵️‍ We beginnen hier!)
//   .__proto__ → Greeting.prototype
//     .__proto__ → React.Component.prototype
//       .__proto__ → Object.prototype (🙅‍ Niet gevonden!)

Maar het zou net zo goed kunnen werken om te bepalen of een class een andere class extend:

console.log(Greeting.prototype instanceof React.Component);
// greeting
//   .__proto__ → Greeting.prototype (🕵️‍ We beginnen hier)
//     .__proto__ → React.Component.prototype (✅ Gevonden!)
//       .__proto__ → Object.prototype

En zo’n check is hoe we kunnen bepalen of iets een React component class of een normale functie is.


Dat is echter niet wat React doet. 😳

Een nadeel van de instanceof oplossing is dat het niet werkt als er meerdere kopiëen van React op de pagina zijn. En de component die we aan het checken zijn iets erft van React.Component van een andere kopie van React. Meerdere kopieën van React mixen in hetzelfde project is sowieso een slecht idee. Maar historisch gezien proberen we zoveel mogelijk problemen te vermijden. (Met Hooks moeten we misschien deduplicatie forceren.)

Een ander mogelijke oplossing zou kunnen zijn om te checken of er een render method aanwezig is op het prototype. Echter, voorheen was het niet duidelijk hoe de component API zou evolueren. Iedere check kost iets dus we wilden er niet meer dan een toevoegen. Het zou ook niet werken als render als een instance method was gedefinieerd, zoals met de class property syntax.

In plaats daarvan voegde React een speciale vlag toe aan de base component. React checkt de aanwezigheid van deze flag en dat is hoe deze weet of iets een React component class is of niet.

In het begin was de flag op de base van de React.Component class zelf:

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

// We kunnen het op deze manier checken
class Greeting extends Component {}
console.log(Greeting.isReactClass); // ✅ Ja

Echter, sommige class implementaties waar we ons op richtten kopiëren static properties niet (of zetten de niet-standaard __proto__), waardoor de vlag kwijtraakte.

Dit is waarom React de vlag verplaatste naar React.Component.prototype:

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

//  We kunnen het op deze manier checken
class Greeting extends Component {}
console.log(Greeting.prototype.isReactComponent); // ✅ Ja

En dat is het enige dat we hoeven te doen om het verschil te zien.

Je vraagt je misschien af waarom het een object is en geen boolean. In de praktijk maakt het niet veel uit maar eerdere versies van Jest (Voordat Jest Goed Was™️) had automocking standaard aan staan. De gegenereerde mocks lieten primitieve properties achterwegen, en braken daarmee de check. Bedankt Jest.

De isReactComponent check wordt vandaag de dag nog steeds gebruikt in React.

Als je React.Component niet extend zal React isReactComponent niet vinden op de prototype en de component niet als class behandelen. Nu weet je waarom het antwoord ‘add extends React.Componentde meeste upvotes heeft op de vraag Cannot call a class as a function. Ten slotte is er een waarschuwing toegevoegd die waarschuwt wanneer prototype.render bestaat maar prototype.isReactComponent niet.


Je zou kunnen zeggen dat dit hele verhaal een beetje flauw is. Het antwoord is immers erg simpel. Maar ik heb er best wat werk in gestopt om uit te kunnen leggen waarom er in React uiteindelijk voor deze oplossing is gekozen, en wat de alternatieven waren.

Naar mijn ervaring is dit vaker het geval met library API’s. Om als API makkelijk te gebruiken te zijn moet je vaak rekening houden met de semantiek (mogelijk voor meerdere talen, inclusief toekomstige veranderingen), snelheidsverbeteringen, ergonomie met en zonder compile-time stappen, de staat van het ecosysteem en packaging oplossingen, vroege waarschuwingen en nog veel meer. Het eindresultaat is misschien niet altijd even elegant, maar het moet wel praktisch zijn.

Als de uiteindelijke API succesvol is, hoeven de gebruikers nooit over dit proces na te denken. Zo kunnen ze zich focussen op het maken van apps.

Maar als je toch nieuwsgierig bent naar hoe het werkt, is het ook fijn om te weten hoe het werkt.