Introducción

Hasta hace unos meses, los componentes de clase de React (también llamados stateful components) estaban de moda. Pero eso se acabó con la llegada de los Hooks. ¿Por qué han dejado de ser una buena idea? En este post vamos a enumerar los principales inconvenientes a la hora de trabajar con componentes de clase en React.

La pesadilla del “this

En componentes de clase, el valor que adopta this dentro de una función dependerá de cómo sea invocada dicha función. Cuando nuestra función es utilizada como manejador de eventos (event handler), el valor al que apunta this será undefined. ¿Por qué? Las declaraciones y expresiones de clase (como por ejemplo nuestra función) se ejecutan en modo estricto, bajo el cual se aplica el binding por defecto a undefined y no a la instancia del componente como cabría esperar.

Más información al respecto aqui: https://medium.freecodecamp.org/this-is-why-we-need-to-bind-event-handlers-in-class-components-in-react-f7ea1a6f93eb

Por tanto, el this en un event handler disparado fuera de nuestro componente de clase, perderá la referencia a la instancia a la que pertenece.

Si ejecutamos el siguiente ejemplo y hacemos click en el botón Change Text, obtendremos el error:

Cannot read property ‘setState’ of undefined
export class MyHelloComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { myText: "hello" };
  }

  onChangeText() {        
    this.setState({ myText: "world" });
  }

  render() {
    return (
      <>
        <h3>{this.state.myText}</h3>
        <button onClick={this.onChangeText}>Change text</button>
      </>
    );
  }
}

La solución pasa por “atar” manualmente el this a la instancia de clase en cada una de las funciones declaradas como métodos de clase. Para ello contamos con dos mecanismos, o bien hacer un binding explícito en el constructor o bien utilizar funciones flecha experimentales (fat arrow) como método de clase (ver linea afectada marcada con el caracter “+”).

export class MyHelloComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { myText: "hello" };
+    this.onChangeText = this.onChangeText.bind(this);
  }

  onChangeText() {        
    this.setState({ myText: "world" });
  }

  render() {
    return (
      <>
        <h3>{this.state.myText}</h3>
        <button onClick={this.onChangeText}>Change text</button>
      </>
    );
  }
}

Demo: https://codesandbox.io/s/71opm61rzq

Esta característica de las clases puede llegar a convertirse en una fuente de problemas. ¿Cuántas veces has olvidado hacer adecuadamente el bind a tus event handlers, obteniendo finalmente un error en tiempo de ejecución?

Estado monolítico y funcionalidad difícilmente extraíble

En un componente de clase, tenemos un único lugar donde definir todo el estado al completo, y al mismo tiempo, una única forma de actualizarlo. ¿Inconveniente? Es complicado separar intereses y extraer la funcionalidad para su reuso. Recordemos que la separación de intereses (separation of concerns) y la reutilización de código (code reuse) son dos de los más importantes principios de programación que aparecerán en cualquier manual de buenas prácticas.

En el siguiente ejemplo, tenemos dos conceptos diferentes encapsulados en el mismo estado de clase:

export class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { color: "teal", name: "John", lastname: "Doe" };
  }

  getFullname() {
    return `${this.state.name} ${this.state.lastname}`;
  }

  render() {
      return (
          <div style={{ background: this.state.color }}>
              <h3>{this.getFullname()}</h3>
          </div>
      )
  }
}

Demo: https://codesandbox.io/s/yp9v4yyr8x

Veamos como podríamos implementar el mismo escenario usando hooks. En este caso podremos separar el estado para cada concepto:

import React from "react";

export const MyComponent = () => {
  const [color, setColor] = React.useState("teal");
  const [clientInfo, setClientInfo] = React.useState({name: 'John', lastname: 'Doe'});

  const getFullname = () => {
      return `${clientInfo.name} ${clientInfo.lastname}`;
  }

  return (
    <div style={{ background: color }}>
      <h3>{getFullname()}</h3>
    </div>
  );
};

Demo: https://codesandbox.io/s/ppom543mj7

Podemos dar un paso más y encapsular la funcionalidad relativa al usuario en un custom hook:

const useClientInfo = (name, lastname) =>  {
  const [clientInfo, setClientInfo] = React.useState({
    name,
    lastname,
  });

  const getFullname = () => {
    return `${clientInfo.name} ${clientInfo.lastname}`;
  };

  return {clientInfo, setClientInfo, getFullname}
}

export const MyComponent = () => {
  const [color, setColor] = React.useState("teal");
  const {getFullname} = useClientInfo('John', 'Doe');

  return (
    <div style={{ background: color }}>
      <h3>{getFullname()}</h3>
    </div>
  );
};

Demo: https://codesandbox.io/s/ly7wlq9mo9

Gestión de intereses relacionados en manejadores separados

Cuando usamos componentes de clase en React, disponemos de distintos eventos durante su ciclo de vida en los cuales ejecutar código: componentDidMount, componentDidUpdate, componentWillUnmount, etc. Podría darse el caso de que cierta funcionalidad relacionada tenga que ser dividida entre los distintos handlers del ciclo de vida del componente. Por ejemplo:

Definamos un componente padre:

export class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { visible: false };
  }

  render() {
    return (
      <>
        {this.state.visible && <MyChildComponent />}
        <button onClick={() => this.setState({ visible: !this.state.visible })}>
          Toggle Child component visibility
        </button>
      </>
    );
  }
}

Ahora, un componente hijo que simplemente mostrará la información de usuario:

export class MyChildComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { name: "John", lastname: "Doe" };
  }

  componentDidMount() {
    console.log("Hey Im mounting");
    console.log(`${this.state.name} ${this.state.lastname}`);
  }

  componentDidUpdate() {
    console.log("Just updating...");
    console.log(`${this.state.name} ${this.state.lastname}`);
  }

  componentWillUnmount() {
    console.log("bye bye, unmounting...");
  }

  render() {
    return (
      <div>
        <h3>
          {this.state.name} {this.state.lastname}
        </h3>
        <input
          value={this.state.name}
          onChange={e => this.setState({ name: e.target.value })}
        />
        <input
          value={this.state.lastname}
          onChange={e => this.setState({ lastname: e.target.value })}
        />
      </div>
    );
  }
}

Demo: https://codesandbox.io/s/oqyo3159jq

Mediante el empleo de hooks podemos agrupar toda esta funcionalidad en una única función:

const MyChildComponent = () => {
  const [userInfo, setUserInfo] = React.useState({name: 'John', lastname: 'Doe'})

  React.useEffect(() => {
    console.log('called when the component is mounted and right after it gets updated');

    return () => console.log('Clean up from the previous render before running effect next time ... ');
  })

  return (
    <div>
      <h3>
        {userInfo.name} {userInfo.lastname}
      </h3>
      <input
        value={userInfo.name}
        onChange={e => setUserInfo({ ...userInfo, name: e.target.value })}
      />
      <input
        value={userInfo.lastname}
        onChange={e => setUserInfo({ ...userInfo, lastname: e.target.value })}
      />
    </div>
  );
}

En este ejemplo, el código declarado dentro del useEffect es ejecutado una vez que el componente se ha montado pero también justo después de cada render. Por otro lado, la función de limpieza (devuelta en el useEffect) se ejecuta después de que el componente se haya montado y después de cada ejecución del efecto.

Demo: https://codesandbox.io/s/5zllr3k09p

Bien, pero … ¿y si quiero ejecutar un trozo de código solo cuando el componente se haya montado y hacer la limpieza cuando el componente vaya a desmontarse? Para ello, podemos jugar con el segundo parámetro del useEffect (en el ejemplo marcado con “-” las lineas que se eliminarían, con “+” las líneas que se añaden).

const MyChildComponent = () => {
  const [userInfo, setUserInfo] = React.useState({name: 'John', lastname: 'Doe'})

  React.useEffect(() => {
-    console.log('called just when the component is mounted and when after it gets updated');
+    console.log('called just when the component is mounted');

-    return () => console.log('Clean up from the previous render before running effect next time ... ');
+    return () => console.log('Clean up executed just when the component gets unmounted ... ');
-  })
+  }, [])

  return (
    <div>
      <h3>
        {userInfo.name} {userInfo.lastname}
      </h3>
      <input
        value={userInfo.name}
        onChange={e => setUserInfo({ ...userInfo, name: e.target.value })}
      />
      <input
        value={userInfo.lastname}
        onChange={e => setUserInfo({ ...userInfo, lastname: e.target.value })}
      />
    </div>
  );
}

Demo: https://codesandbox.io/s/q91qjql8r4

Ruido al utilizar High Order Components

Un HOC o High Order Component es una excelente forma de añadir nuevas capacidades a tu componente mediante composición, sin embargo:

  • Necesitas refactorizar su interfaz para poder aplicarlo.

  • Encadenar diversos HOC simultáneamente puede ser una pesadilla.

Veamos un ejemplo mínimo. El siguiente componente se emplea para saludar al usuario que está actualmente logado:

import React from "react";

export const MyComponent = (props) => {
  return (
    <>
      <h3>Hello: </h3>
    </>
  )
}

Para inyectar el usuario que está actualmente logado podemos crear un HOC:

const withUserInfo = (ComponentToWrap) => (props) =>
  <>
    <ComponentToWrap {...props} user="John" />
  </>
Let's make usage of this HoC in MyComponent

- export const MyComponent = (props) => {
+ const 

Podemos hacer uso de este HOC en MyComponent del siguiente modo:

const MyComponentInner = (props) => {  
  return (
    <>
      <h3>Hello: {props.user}</h3>
    </>
  )
}

export const MyComponent = withUserInfo()

Demo: https://codesandbox.io/s/l7qznjjyw9

Esta forma de añadir funcionalidad es bastante potente, pero, ¿no sería más sencillo y expresivo un mecanismo con el que podamos decir: “quiero usar esta funcionalidad”? Veamos como implementar este comportamiento utilizando hooks:

import React from "react";

const useUserInfo = () => {
  const [userInfo, setUserInfo] = React.useState('John');

  return {userInfo, setUserInfo}
}

export const MyComponent = (props) => {
  const {userInfo} = useUserInfo();

  return (
    <>
      <h3>Hello: {userInfo}</h3>
    </>
  )
}

Demo: https://codesandbox.io/s/pwj6z446xq

Ventajas:

  • No necesitamos reescribir el interfaz de nuestro componente: borrar el export, crear un componente para envolverlo, etc.

  • Sin complicaciones al tener anidando HOCs.

  • El contrato es claro, no tenemos que adivinar que propiedades está inyectando el HOC a nuestro componente.

Migrando componentes de su forma funcional a clase, y también en el sentido contrario

Cuando iniciamos la implementación de un componente tratamos de mantenerlo sencillo, por lo que es típico comenzar con un componente funcional. Un ejemplo simple podría ser el siguiente: mostrar un mensaje de bienvenida a un usuario (nos lo informa el padre a través de la propiedad userName).

import React from "react";

export const MyComponent = () => {
  return (
    <>
      <MyChildComponent userName="John"/>
    </>
  )
}

export const MyChildComponent = (props) => {
  return (
    <>
      <h3>Hello: {props.userName}</h3>      
    </>
  )
}

Demo: https://codesandbox.io/s/985wyr1olw

Supongamos que, en lugar de recibirlo desde el padre, queremos almacenarlo en el estado local de nuestro componente. Una aproximación clásica sería migrar nuestro componente a su forma stateful, es decir, refactorizarlo por completo, prestando atención de no equivocarnos con el this.state, añadiendo su constructor, implementando el método render, etc. Algo tal que así:

export const MyComponent = () => {
  return (
    <>
      <MyChildComponent/>
    </>
  )
}

export class MyChildComponent extends React.Component {  
  constructor(props) {
    super(props);
    this.state = {userName: 'John'}
  } 

  render() {
      return (
        <>  
         <h3>Hello: {this.state.userName}</h3>      
        </>
      )
  }
}

Demo: https://codesandbox.io/s/40l5k4q1o9

¿Cómo podría haber sido este refactor utilizando hooks? Tan sencillo como esto:

export const MyComponent = () => {
  return (
    <>
     <MyChildComponent/>
    </>
  )
}

export const MyChildComponent = (props) => {
  const [userName]  = React.useState('John');
  return (
    <> 
     <h3>Hello: {userName}</h3>      
    </>
  )
}

Demo: https://codesandbox.io/s/jlkoxm6plw

Combinando componentes funcionales y de clase

Por último, pero no menos importante, en un proyecto de dimensiones considerables, una aproximación mixta donde componentes funcionales y de clase tengan que convivir juntos podría traer diversos inconvenientes de mantenibilidad:

  • Inconsistencia en nuestra base de código.

  • Dificultad añadida para que nuevos desarrolladores se inicien en el proyecto, teniendo que aprender ambas formas y decidir cuando es más conveniente una u otra, o cuando deben refactorizar un componente existente en uno u otro sentido.

  • La refactorización de componentes, ya sea en un sentido u otro, es una tarea propensa a errores, por ejemplo: se nos olvida eliminar el this, nos equivocamos en la sintaxis de clase vs función, errores con la función de render, etc.

Resumiendo

Antes de tirarnos a la piscina con la nueva característica de moda, es importante aprender por qué la necesitamos y que problemas viene a resolver. Esperamos que este artículo pueda ayudaros a entender mejor por qué los hooks han generado tanta expectación en la comunidad y se han convertido en un estándar de facto. En los siguientes artículos de esta serie comenzaremos a indagar en los detalles que hacen a los hooks tan poderosos, mostrando su cara amable y también los casos frontera, basándonos en casos de uso en proyectos reales. ¡Estad atentos!

Esperamos que hayáis disfrutado el artículo. ¡Muchas gracias!