Comment écrire un composant React sans utiliser de classes ou de hooks

Avec la sortie de React Hooks, j'ai vu de nombreux articles comparant les composants de classe aux composants fonctionnels. Les composants fonctionnels ne sont pas nouveaux dans React, mais il n'était pas possible avant la version 16.8.0 de créer un composant avec état avec accès aux hooks de cycle de vie en utilisant uniquement une fonction. Ou était-ce?

Appelez-moi un pédant (beaucoup de gens le font déjà!) Mais quand nous parlons de composants de classe, nous parlons techniquement de composants créés par des fonctions. Dans cet article, j'aimerais utiliser React pour montrer ce qui se passe réellement lorsque nous écrivons une classe en JavaScript.

Classes vs fonctions

Tout d'abord, je voudrais montrer très brièvement comment ce que l'on appelle communément les composants fonctionnels et de classe sont liés les uns aux autres. Voici un composant simple écrit en classe:

class Hello extends React.Component { render() { return 

Hello!

} }

Et ici, il s'écrit en fonction:

function Hello() { return 

Hello!

}

Notez que le composant fonctionnel n'est qu'une méthode de rendu. Pour cette raison, ces composants n'ont jamais pu conserver leur propre état ou avoir des effets secondaires à certains moments de leur cycle de vie. Depuis React 16.8.0, il est possible de créer des composants fonctionnels avec état grâce à des hooks, ce qui signifie que nous pouvons transformer un composant comme celui-ci:

class Hello extends React.Component { state = { sayHello: false } componentDidMount = () => { fetch('greet') .then(response => response.json()) .then(data => this.setState({ sayHello: data.sayHello }); } render = () => { const { sayHello } = this.state; const { name } = this.props; return sayHello ? 

{`Hello ${name}!`}

: null; } }

Dans un composant fonctionnel comme celui-ci:

function Hello({ name }) { const [sayHello, setSayHello] = useState(false); useEffect(() => { fetch('greet') .then(response => response.json()) .then(data => setSayHello(data.sayHello)); }, []); return sayHello ? 

{`Hello ${name}!`}

: null; }

Le but de cet article n'est pas de prétendre que l'un est meilleur que l'autre, car il existe déjà des centaines d'articles sur ce sujet! La raison de montrer les deux composants ci-dessus est que nous puissions être clairs sur ce que React en fait réellement.

Dans le cas du composant de classe, React crée une instance de la classe à l'aide du newmot - clé:

const instance = new Component(props); 

Cette instance est un objet. Lorsque nous disons qu'un composant est une classe, ce que nous voulons dire, c'est qu'il s'agit d'un objet. Ce nouveau composant objet peut avoir son propre état et ses propres méthodes, dont certaines peuvent être des méthodes de cycle de vie (render, componentDidMount, etc.) que React appellera aux points appropriés pendant la durée de vie de l'application.

Avec un composant fonctionnel, React l'appelle simplement comme une fonction ordinaire (car c'est une fonction ordinaire!) Et il renvoie soit du HTML, soit plus de composants React.

Les méthodes permettant de gérer l'état des composants et de déclencher des effets à des points au cours du cycle de vie du composant doivent maintenant être importées si elles sont nécessaires. Ces derniers fonctionnent entièrement en fonction de l'ordre dans lequel ils sont appelés par chaque composant qui les utilise, car ils ne savent pas quel composant les a appelés. C'est pourquoi vous ne pouvez appeler des hooks qu'au niveau supérieur du composant et ils ne peuvent pas être appelés conditionnellement.

La fonction constructeur

JavaScript n'a pas de classes. Je sais que ça a l'air d'avoir des classes, nous venons d'en écrire deux! Mais sous le capot, JavaScript n'est pas un langage basé sur des classes, il est basé sur un prototype. Les classes ont été ajoutées avec la spécification ECMAScript 2015 (également appelée ES6) et ne sont qu'une syntaxe plus claire pour les fonctionnalités existantes.

Essayons de réécrire un composant de classe React sans utiliser la syntaxe de classe. Voici le composant que nous allons recréer:

class Counter extends React.Component { constructor(props) { super(props); this.state = { count: 0 } this.handleClick = this.handleClick.bind(this); } handleClick() { const { count } = this.state; this.setState({ count: count + 1 }); } render() { const { count } = this.state; return (  +1 

{count}

); } }

Cela rend un bouton qui incrémente un compteur lorsqu'on clique dessus, c'est un classique! La première chose que nous devons créer est la fonction constructeur, elle effectuera les mêmes actions que la constructorméthode de notre classe effectue en dehors de l'appel à supercar c'est une chose réservée à la classe.

function Counter(props) { this.state = { count: 0 } this.handleClick = this.handleClick.bind(this); } 

C'est la fonction que React appellera avec le newmot - clé. Lorsqu'une fonction est appelée avec newelle est traitée comme une fonction constructeur; un nouvel objet est créé, la thisvariable est pointée vers lui et la fonction est exécutée avec le nouvel objet utilisé partout où il thisest mentionné.

Ensuite, nous avons besoin de trouver un foyer pour renderet handleClickméthodes et que nous devons parler de la chaîne prototype.

La chaîne prototype

JavaScript permet l'héritage des propriétés et des méthodes entre les objets via quelque chose connu sous le nom de chaîne de prototypes.

Eh bien, je dis héritage, mais je veux dire en fait la délégation. Contrairement aux autres langages dotés de classes, où les propriétés sont copiées d'une classe vers ses instances, les objets JavaScript ont un lien protoype interne qui pointe vers un autre objet. Lorsque vous appelez une méthode ou tentez d'accéder à une propriété sur un objet, JavaScript vérifie d'abord la propriété sur l'objet lui-même. S'il ne le trouve pas, il vérifie le prototype de l'objet (le lien vers l'autre objet). S'il ne peut toujours pas le trouver, il vérifie le prototype du prototype et ainsi de suite jusqu'à ce qu'il le trouve ou soit à court de prototypes à vérifier.

D'une manière générale, tous les objets en JavaScript ont Objecten haut de leur chaîne de prototypes; c'est ainsi que vous avez accès à des méthodes telles que toStringet hasOwnPropertysur tous les objets. La chaîne se termine lorsqu'un objet est atteint avec nullcomme prototype, c'est normalement à Object.

Essayons de rendre les choses plus claires avec un exemple.

const parentObject = { name: 'parent' }; const childObject = Object.create(parentObject, { name: { value: 'child' } }); console.log(childObject); 

Nous créons d'abord parentObject. Parce que nous avons utilisé la syntaxe littérale de l'objet, cet objet sera lié Object. Ensuite, nous utilisons Object.createpour créer un nouvel objet en utilisant parentObjectcomme prototype.

Maintenant, lorsque nous utilisons console.logpour imprimer notre, childObjectnous devrions voir:

console output of childObject

The object has two properties, there is the name property which we just set and the __proto___ property. __proto__ isn't an actual property like name, it is an accessor property to the internal prototype of the object. We can expand these to see our prototype chain:

expanded output of childObject

The first __proto___ contains the contents of parentObject which has its own __proto___ containing the contents of Object. These are all of the properties and methods that are available to childObject.

It can be quite confusing that the prototypes are found on a property called __proto__! It's important to realise that __proto__ is only a reference to the linked object. If you use Object.create like we have above, the linked object can be anything you choose, if you use the new keyword to call a constructor function then this linking happens automatically to the constructor function's prototype property.

Ok, back to our component. Since React calls our function with the new keyword, we now know that to make the methods available in our component's prototype chain we just need to add them to the prototype property of the constructor function, like this:

Counter.prototype.render = function() { const { count } = this.state; return (  +1 

{count}

); }, Counter.prototype.handleClick = function () { const { count } = this.state; this.setState({ count: count + 1 }); }

Static Methods

This seems like a good time to mention static methods. Sometimes you might want to create a function which performs some action that pertains to the instances you are creating - but it doesn't really make sense for the function to be available on each object's this. When used with classes they are called Static Methods. I'm not sure if they have a name when not used with classes!

We haven't used any static methods in our example, but React does have a few static lifecycle methods and we did use one earlier with Object.create. It's easy to declare a static method on a class, you just need to prefix the method with the static keyword:

class Example { static staticMethod() { console.log('this is a static method'); } } 

And it's equally easy to add one to a constructor function:

function Example() {} Example.staticMethod = function() { console.log('this is a static method'); } 

In both cases you call the function like this:

Example.staticMethod() 

Extending React.Component

Our component is almost ready, there are just two problems left to fix. The first problem is that React needs to be able to work out whether our function is a constructor function or just a regular function. This is because it needs to know whether to call it with the new keyword or not.

Dan Abramov wrote a great blog post about this, but to cut a long story short, React looks for a property on the component called isReactComponent. We could get around this by adding isReactComponent: {} to Counter.prototype (I know, you would expect it to be a boolean but isReactComponent's value is an empty object. You'll have to read his article if you want to know why!) but that would only be cheating the system and it wouldn't solve problem number two.

In the handleClick method we make a call to this.setState. This method is not on our component, it is "inherited" from React.Component along with isReactComponent. If you remember the prototype chain section from earlier, we want our component instance to first inherit the methods on Counter.prototype and then the methods from React.Component. This means that we want to link the properties on React.Component.prototype to Counter.prototype.__proto__.

Fortunately there's a method on Object which can help us with this:

Object.setPrototypeOf(Counter.prototype, React.Component.prototype); 

It Works!

That's everything we need to do to get this component working with React without using the class syntax. Here's the code for the component in one place if you would like to copy it and try it out for yourself:

function Counter(props) { this.state = { count: 0 }; this.handleClick = this.handleClick.bind(this); } Counter.prototype.render = function() { const { count } = this.state; return (  +1 

{count}

); } Counter.prototype.handleClick = function() { const { count } = this.state; this.setState({ count: count + 1 }); } Object.setPrototypeOf(Counter.prototype, React.Component.prototype);

As you can see, it's not as nice to look at as before. In addtion to making JavaScript more accessible to developers who are used to working with traditional class-based languages, the class syntax also makes the code a lot more readable.

I'm not suggesting that you should start writing your React components in this way (in fact, I would actively discourage it!). I only thought it would be an interesting exercise which would provide some insight into how JavaScript inheritence works.

Although you don't need to understand this stuff to write React components, it certainly can't hurt. I expect there will be occassions when you are fixing a tricky bug where understanding how prototypal inheritence works will make all the difference.

I hope you have found this article interesting and/or enjoyable. You can find more posts that I have written on my blog at hellocode.dev. Thank you.