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 new
palabra 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 constructor
método de nuestra clase aparte de la llamada a super
porque 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 new
palabra clave. Cuando se llama a una función con new
, se trata como una función constructora; se crea un nuevo objeto, la this
variable se apunta a él y la función se ejecuta con el nuevo objeto que se utiliza donde this
se menciona.
A continuación, tenemos que encontrar un hogar para los render
y handleClick
mé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 Object
en la parte superior de su cadena de prototipos; así es como tiene acceso a métodos como toString
y hasOwnProperty
en todos los objetos. La cadena termina cuando se llega a un objeto con null
su 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.create
para crear un nuevo objeto utilizando parentObject
como prototipo.
Ahora, cuando usamos console.log
para imprimir nuestro childObject
deberíamos ver:
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:
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.