leanmind logo leanmind text logo

Blog

BDD

Behaviour-driven Development es una técnica para tomar mejores requisitos de producto.

React custom hooks: diseño y testing

Por Adrián Ferrera González

React es una de las librerías utilizadas para desarrollar aplicaciones front-end más conocidas a día de hoy. Esta librería se caracteriza por ser una de las más estables a lo largo de los últimos años y al mismo tiempo, de ofrecer nuevas características sin dar por obsoletas las ya existentes anteriormente.

No es hasta la versión 16.8.0 que fueron introducidos los hooks de forma oficial.

¿Qué son los hooks?

Los hooks no son más que una función utilizada por un componente, cuya responsabilidad se quiere extraer del componente. Solo los Functional component pueden usarlas y como regla no escrita, todas ellas deben empezar por el sufijo use.

Por ejemplo, la librería ya nos ofrece un conjunto de hooks, como:

Puedes encontrar tanto que hace cada uno de ellos, como otros hooks que pueden ser de utilidad en la documentación oficial.

¿Cómo se usan?

Lo importante de los hooks es que, además de existir un conjunto de ellos ya definidos por la librería, nosotros podemos crear uno propio. El caso más típico suele ser el de hacer una petición asíncrona a una api:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export const MyComponent = ({ repository }) => {
  // Define multiple internal states
  const [loading, setLoading] = React.useState(false);
  const [profile, setProfile] = React.useState({});
  const [error, setError] = React.useState(null);

  // Define the code to execute component will mount
  React.useEffect(() => {
    setLoading(true);
    repository.getProfile()
      .then(setProfile)
      .catch(setError)
      .finally(() => setLoading(false));
  },[repository]);

  return (
        <div className="myComponent">
            {loading ? <Spinner /> : <Profile value={profile}/>}
            {error ? <ErrorMessage>{error.message}</ErrorMessage>}
        </div>
      )
};

Analicemos el siguiente código:

Ahora bien, este código puede coincidir de forma directa con el de otros componentes que también consuman el perfil de usuario. Por lo que no tiene sentido que tengamos este código repetido en todos ellos y lo conveniente sería llevarlo a una función común.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const useProfile = (repository) => {
    // Define multiple internal states
    const [loading, setLoading] = React.useState(false);
    const [profile, setProfile] = React.useState({});
    const [error, setError] = React.useState(null);

    // Define the code to execute component will mount
    const getProfile = () => {
        setLoading(true);
        repository.getProfile()
            .then(setProfile)
            .catch(setError)
            .finally(() => setLoading(false));
    };
    return {loading, profile, error, getProfile }
};

Esta función recibe por parámetro el repository y devuelve un objeto con una funcion que hace uso de un estado (useState). Dicho estado, será el del componente que consuma esta función. El objeto devuelto expone también los valores del estado, por lo que además le hemos ocultado los setters a los componentes que consumen dicha función.

Para entender esto, tenemos que entender el ciclo de vida de los Function Components. Cada vez que cambie el estado (en este caso los valores devueltos por el hook) o que las props que el componente recibe cambien: el componente será renderizado nuevamente. Este estado se gestiona dentro del hook y es único por componente. Es decir, no es un singleton, sino que es simplemente una función helper que reutilizamos. Traducción: El componente no almacena ningún valor, solo lo recibe del hook y lo pinta. Si hay un cambio en alguna de sus props (en este caso repository), el componente se pinta de nuevo. No es el caso, pero si tuviésemos alguna acción dentro del componente que cambiase el valor de repository, este ejecutaría el useEffect nuevamente.

Si quisiésemos compartir el mismo estado entre distintos componentes, tendríamos que usar dicho hook en un componente que esté por arriba y pasarlo a través de las props a los hijos (Higher Order Component).

O si quisiéramos ir un paso más allá: crearíamos un contexto que envolviese a nuestros componentes y recuperaríamos el valor de dicho contexto, a través de useContext(MyContext), independientemente de la jerarquía (siempre y cuando dicho componente o sus ancestros estén envueltos en dicho contexto).

Nuestro componente usaría así la función/customHook:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
export const MyComponent = ({ repository }) => {
  const {loading, profile, error, getProfile} = useProfile(repository);
  React.useEffect(() => { getProfile() }, [repository]);
  return (
        <div className="myComponent">
            {loading ? <Spinner /> : <Profile value={profile}/>}
            {error ? <ErrorMessage>{error.message}</ErrorMessage>}
        </div>
      )
};

Testing

Ahora bien, ¿es necesario testear nuestros hooks? La respuesta es depende. Si podemos probar nuestra funcionalidad de forma plena directamente desde el test del propio componente, no sería necesario.

Sin embargo, no siempre es así. O no siempre tenemos por qué desarrollar el hook a partir de un componente, por lo que en estos casos, si puede resultar interesante el desarrollar unos test para el mismo.

Ahora bien, aunque un hook sea una función, dentro va a utilizar determinadas funcionalidades definidas por react que no podemos emular de forma simple. Es por ello que @testing-library/react tiene un complemento llamado @testing-library/react-hooks. Con esta herramienta, seremos capaces de llamar al comportamiento y verificar su estado.

Para ello, primero debemos instalar las dependencias:

npm i --save-dev jest @testing-library/react-hooks

Y definiremos el test de la forma más simple posible. En este caso, llamaremos a dicha función pasándole un mock del repository que simule el comportamiento real del fetch y comprobaremos que los valores retornados han cambiado en base a dicha respuesta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import { act, renderHook } from '@testing-library/react-hooks';
import { useProfile } from './MyComponent';
import { buildProfile } from './builders'

describe('useProfile', () => {
  it('retrieves the profile', async () => {
    // Define data
    const expectedProfile = buildProfile();
    const repository = {
      getProfile: jest.fn(() => Promise.resolve(expectedProfile)),
    };
    // Mount hook
    const {
      loading, profile, error, getProfile,
    } = renderHook(() => useProfile(repository));
    // Execute the action
    await act(async () => {
      await getProfile();
    });
    // Check the values
    expect(loading).toBeFalsy();
    expect(profile).toEqual(expectedProfile);
    expect(error).toBeNull();
  });
});

Lo que ocurre en el ejemplo anterior:

De la misma forma, podríamos probar también casos de error o llevarnos esta misma mecánica a casos más complicados, como podría ser un hook que envuelve la lógica para gestionar el estado de una aplicación redux, a través de redux-thunk.

Espero que este ejemplo os sirva de ayuda en el futuro, no solo para perderle el miedo a los hooks, sino también para poder crear tests de forma sencilla y dar mayor robustez a vuestro código.

Publicado el 20/04/2020 por
Adrián image

Adrián Ferrera González

https://adrianferrera.dev

¿Quieres más? te invitamos a suscribirte a nuestro boletín para avisarte cada vez que recopilemos contenido de calidad que compartir.

Si disfrutas leyendo nuestro blog, ¿imaginas lo divertido que sería trabajar con nosotros? ¿te gustaría?

Impulsamos el crecimiento profesional de tu equipo de developers