Cómo escribir un componente de React sin usar clases o ganchos

Con el lanzamiento de React Hooks, he visto muchas publicaciones que comparan componentes de clase con componentes funcionales. Los componentes funcionales no son nada nuevo en React, sin embargo, antes de la versión 16.8.0 no era posible crear un componente con estado con acceso a los ganchos del ciclo de vida usando solo una función. ¿O fue?

Llámame pedante (¡mucha gente ya lo hace!) Pero cuando hablamos de componentes de clase, técnicamente estamos hablando de componentes creados por funciones. En esta publicación, me gustaría usar React para demostrar lo que realmente está sucediendo cuando escribimos una clase en JavaScript.

Clases vs Funciones

En primer lugar, me gustaría mostrar muy brevemente cómo se relacionan entre sí lo que comúnmente se conoce como componentes funcionales y de clase. Aquí hay un componente simple escrito como clase:

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

Hello!

} }

Y aquí está escrito como función:

function Hello() { return 

Hello!

}

Observe que el componente funcional es solo un método de renderizado. Debido a esto, estos componentes nunca pudieron mantener su propio estado o realizar efectos secundarios en algunos puntos durante su ciclo de vida. Desde React 16.8.0 ha sido posible crear componentes funcionales con estado gracias a los ganchos, lo que significa que podemos convertir un componente como este:

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; } }

En un componente funcional como este:

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; }

El propósito de este artículo no es discutir que uno es mejor que el otro, ¡ya que hay cientos de publicaciones sobre ese tema! La razón para mostrar los dos componentes anteriores es para que podamos tener claro lo que React realmente hace con ellos.

En el caso del componente de clase, React crea una instancia de la clase usando la newpalabra clave:

const instance = new Component(props); 

Esta instancia es un objeto. Cuando decimos que un componente es una clase, lo que realmente queremos decir es que es un objeto. Este nuevo componente de objeto puede tener su propio estado y métodos, algunos de los cuales pueden ser métodos de ciclo de vida (render, componentDidMount, etc.) que React llamará en los puntos apropiados durante la vida útil de la aplicación.

Con un componente funcional, React simplemente lo llama como una función ordinaria (¡porque es una función ordinaria!) Y devuelve HTML o más componentes React.

Los métodos con los que manejar el estado del componente y los efectos de activación en puntos durante el ciclo de vida del componente ahora deben importarse si es necesario. Estos funcionan enteramente en función del orden en que son llamados por cada componente que los usa, ya que no saben qué componente los ha llamado. Es por eso que solo puede llamar a hooks en el nivel superior del componente y no se pueden llamar condicionalmente.

La función constructora

JavaScript no tiene clases. Sé que parece que tiene clases, ¡acabamos de escribir dos! Pero bajo el capó JavaScript no es un lenguaje basado en clases, está basado en prototipos. Las clases se agregaron con la especificación ECMAScript 2015 (también conocida como ES6) y son solo una sintaxis más limpia para la funcionalidad existente.

Intentemos reescribir un componente de la clase React sin usar la sintaxis de la clase. Aquí está el componente que vamos a recrear:

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}

); } }

Esto genera un botón que incrementa un contador cuando se hace clic, ¡es un clásico! Lo primero que tenemos que crear es la función constructora, esta realizará las mismas acciones que realiza el constructormétodo de nuestra clase aparte de la llamada a superporque eso es algo exclusivo de la clase.

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

Esta es la función que React llamará con la newpalabra clave. Cuando se llama a una función con new, se trata como una función constructora; se crea un nuevo objeto, la thisvariable se apunta a él y la función se ejecuta con el nuevo objeto que se utiliza donde thisse menciona.

A continuación, tenemos que encontrar un hogar para los rendery handleClickmétodos y para eso tenemos que hablar de la cadena de prototipo.

La cadena de prototipos

JavaScript permite la herencia de propiedades y métodos entre objetos a través de algo conocido como cadena de prototipos.

Bueno, digo herencia, pero en realidad me refiero a delegación. A diferencia de otros lenguajes con clases, donde las propiedades se copian de una clase a sus instancias, los objetos JavaScript tienen un enlace prototipo interno que apunta a otro objeto. Cuando llama a un método o intenta acceder a una propiedad en un objeto, JavaScript primero busca la propiedad en el objeto mismo. Si no puede encontrarlo allí, verifica el prototipo del objeto (el enlace al otro objeto). Si aún no puede encontrarlo, entonces verifica el prototipo del prototipo y así sucesivamente hasta que lo encuentre o se quede sin prototipos para verificar.

En términos generales, todos los objetos en JavaScript tienen Objecten la parte superior de su cadena de prototipos; así es como tiene acceso a métodos como toStringy hasOwnPropertyen todos los objetos. La cadena termina cuando se llega a un objeto con nullsu prototipo, normalmente en Object.

Intentemos aclarar las cosas con un ejemplo.

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

Primero creamos parentObject. Porque hemos utilizado la sintaxis literal de objeto a la que se vinculará este objeto Object. A continuación, usamos Object.createpara crear un nuevo objeto utilizando parentObjectcomo prototipo.

Ahora, cuando usamos console.logpara imprimir nuestro childObjectdeberíamos ver:

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.