Overreacted

Pourquoi écrit-on super(props) ?

2018 M11 30 • ☕️ 6 min read

Il paraît que les Hooks sont le nouveau truc cool. Ironiquement, je souhaite commencer ce blog en parlant d’aspects amusants sur les composants écrits sous forme de classes. Incroyable !

Ces petits pièges ne sont pas importants pour une utilisation productive de React. Mais vous les trouverez peut-être amusants si vous aimez fouiller pour comprendre comment les choses marchent.

Voici le premier.


J’ai écrit super(props) bien plus souvent que je ne veux bien l’admettre :

class Checkbox extends React.Component {
  constructor(props) {
    super(props);    this.state = { isOn: true };
  }
  // ...
}

Bien sûr, la proposition Class Fields nous permet de laisser tomber le formalisme :

class Checkbox extends React.Component {
  state = { isOn: true };
  // ...
}

Une syntaxe similaire était prévue quand React 0.13 a commencé à prendre en charge les classes en 2015. Définir un constructor et appeler super(props) a toujours été considéré comme une solution temporaire en attendant que les initialiseurs de champs nous fournissent une alternative plus ergonomique.

Mais revenons à cet exemple qui se limite aux syntaxes ES2015 :

class Checkbox extends React.Component {
  constructor(props) {
    super(props);    this.state = { isOn: true };
  }
  // ...
}

Pourquoi appelle-t-on super ? Peut-on ne pas l’appeler ? Si on doit l’appeler, que se passe-t-il si on ne passe pas props ? Y’a-t-il d’autres arguments ? Allons vérifier tout ça.


En JavaScript, super fait référence au constructeur de la classe parente. (Dans notre exemple, ça pointe sur l’implémentation de React.Component.)

Point important, vous ne pouvez utiliser this dans un constructeur qu’après avoir appelé le constructeur parent (si vous avez fait un extends spécifique). JavaScript ne vous laissera pas le faire trop tôt :

class Checkbox extends React.Component {
  constructor(props) {
    // 🔴 On ne peut pas utiliser `this` à ce stade
    super(props);
    // ✅ Mais maintenant c’est bon !
    this.state = { isOn: true };
  }
  // ...
}

Il y a une bonne raison pour laquelle JavaScript vous force à exécuter le constructeur parent avant de vous laisser toucher à this. Imaginez cette hiérarchie de classes :

class Person {
  constructor(name) {
    this.name = name;
  }
}

class PolitePerson extends Person {
  constructor(name) {
    this.greetColleagues(); // 🔴 Ça c’est interdit, lisez la raison dessous
    super(name);
  }
  greetColleagues() {
    alert('Salut les gens !');
  }
}

Imaginez que l’utilisation de this avant l’appel à super soit permis. Un mois plus tard, nous changerons peut-être greetColleagues pour inclure le nom de la personne dans le message :

  greetColleagues() {
    alert('Salut les gens !');
    alert('Je m’appelle ' + this.name + ', enchantée !');
  }

Mais nous avons perdu de vue que this.greetColleagues() est appelée avant que notre appel à super() ait une chance d’initialiser this.name. Du coup this.name n’est même pas encore défini ! Comme vous pouvez le voir, il peut être délicat de bien comprendre le fonctionnement de ce genre de code.

Pour éviter ce genre de pièges, JavaScript exige que si vous utilisez this dans un constructeur, vous devez appeler super d’abord. Que la classe parente puisse faire son boulot ! Et cette limitation s’applique également aux composants React définis par des classes :

  constructor(props) {
    super(props);
    // ✅ On peut utiliser `this` désormais
    this.state = { isOn: true };
  }

Ce qui nous laisse avec une autre question : pourquoi passer props ?


Vous pensez peut-être que refiler props à super est nécessaire afin que le constructeur React.Component d’origine puisse initialiser this.props ?

// Dans React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

Et ce n’est pas très éloigné de la vérité—en fait, c’est ce que ça fait.

Et pourtant, même si vous appelez super() sans passer props en argument, vous arriveriez toujours à accéder à this.props dans le render et d’autres méthodes. (Si vous ne me croyez pas, essayez par vous-même !)

Mais comment ça marche ? Eh bien, il se trouve que React affecte aussi props sur l’instance juste après avoir appelé votre constructeur.

// Dans React
const instance = new YourComponent(props);
instance.props = props;

Donc même si vous oubliez de passer props à super(), React les définirait tout de même par la suite. Il y a une raison à ça.

Quand React a commencé à prendre en charge les classes, il ne s’est pas limité aux classes ES6. L’objectif était de prendre en charge la plus large gamme d’abstractions de classes possible. On n’était pas sûrs des potentiels de succès de ClojureScript, CoffeeScript, ES6, Fable, Scala.js, TypeScript, ou d’autres solutions pour définir des composants. Résultat, React évitait volontairement d’avoir une opinion trop tranchée sur la nécessité d’appeler super()—même si c’est le cas dans les classes ES6.

Alors, devriez-vous juste écrire super() au lieu de super(props) ?

Probablement pas, car ça pourrait causer de la confusion. Bien sûr, React définira par la suite this.props, après que votre constructeur aura été exécuté. Mais this.props n’en serait pas moins undefined entre l’appel à super et la fin de votre constructeur :

// Dans React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Dans votre code
class Button extends React.Component {
  constructor(props) {
    super(); // 😬 On a oublié de passer les props
    console.log(props); // ✅ {}
    console.log(this.props); // 😬 undefined  }
  // ...
}

Déboguer ce type de situation peut même être encore plus difficile si le souci survient au sein d’une méthode appelée depuis le constructeur. Et voilà pourquoi je recommande de toujours appeler super(props), même si ce n’est pas strictement nécessaire :

class Button extends React.Component {
  constructor(props) {
    super(props); // ✅ On a passé les props
    console.log(props); // ✅ {}
    console.log(this.props); // ✅ {}
  }
  // ...
}

Ça garantit que this.props est défini même avant la fin du constructeur.


Si vous utilisez React depuis longtemps, un dernier point vous chatouille peut-être.

Vous avez peut-être remarqué que lorsque vous utilisez l’API de Contextes dans des classes (aussi bien l’ancienne basée sur contextTypes que la plus récente, basée sur contextType, apparue dans React 16.6), context est passé en second argument au constructeur.

Alors pourquoi n’écrit-on pas plutôt super(props, context) ? On pourrait, mais le contexte est utilisé moins souvent, de sorte qu’on trébuche moins là-dessus.

Avec la proposition Class Fields, ce type de piège disparaît de toutes façons presque entièrement. Sans constructeur explicite, tous les arguments sont transmis automatiquement. C’est ce qui permet à une expression comme state = {} de référencer this.props ou this.context si nécessaire.

Avec les Hooks, on n’a même pas besoin de super ou this. Mais c’est un sujet pour un autre jour.