En este artículo, crearemos una aplicación en React utilizando componentes de clase. Luego, lo convertiremos en componentes funcionales usando React Hooks paso a paso.
Al crear esta aplicación, aprenderás:
- Cómo hacer llamadas a una API
- Cómo implementar funcionalidad de cargar más
- Cómo depurar problemas de la aplicación
- Cómo usar async/await
- Cómo actualizar el componente cuando algo cambia
- Cómo solucionar el problema del bucle infinito en el hook useEffect
- Cómo refactorizar componentes basados en clases en componentes funcionales con Hooks
y mucho más.
Entonces empecemos.
Configuración inicial del proyecto
Crea un nuevo proyecto usando create-react-app
:
npx create-react-app class-to-hooks-refactoring
Una vez tu proyecto este creado, elimina todos los archivos de la carpeta src
y crea el archivo index.js
y el archivo styles.css
dentro de la carpeta src
. También, crea una carpeta components
dentro de la carpeta src
.
Instala la librería axios
ejecutando el siguiente comando desde la carpeta de tu proyecto:
yarn add axios@0.21.1
Abre el archivo styles.css
y agrega el contenido desde este repositorio de GitHub.
Cómo crear las páginas iniciales
Crea un nuevo archivo llamado Header.js
dentro de la carpeta components
con el siguiente contenido:
import React from "react";
const Header = () => {
return <h1 className="header">Random Users</h1>;
};
export default Header;
Crea un nuevo archivo llamado App.js
dentro de la carpeta src
con el siguiente contenido:
import React from 'react';
import Header from './components/Header';
export default class App extends React.Component {
render() {
return (
<div className="main-section">
<Header />
<h2>App Component</h2>
</div>
);
}
}
Ahora, abre el archivo index.js
y agrega el siguiente contenido en él:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import './styles.css';
ReactDOM.render(<App />, document.getElementById('root'));
Ahora, inicia la aplicación ejecutando el comando yarn start
desde la terminal.
Podrás ver la siguiente pantalla sí accedes a la aplicación en http://localhost:3000/.
Cómo hacer llamadas a una API
Usaremos la API de Random Users para obtener una lista de usuarios aleatorios.
Así que abre tu archivo App.js
y agrega el método componentDidMount
dentro del componente:
componentDidMount() {
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
console.log(response.data);
})
.catch((error) => console.log('error', error));
}
También, importa axios
en la parte superior del archivo:
import axios from 'axios';
Todo su archivo App.js
se verá así ahora:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
export default class App extends React.Component {
componentDidMount() {
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
console.log(response.data);
})
.catch((error) => console.log('error', error));
}
render() {
return (
<div className="main-section">
<Header />
<h2>App Component</h2>
</div>
);
}
}
Aquí, estamos haciendo una llamada al API para obtener inicialmente una lista de 10 registros en la URL https://randomuser.me/api/?page=0&results=10
.
Ahora, si verificas la aplicación, verás la respuesta del API en la consola.
Ahora, declaremos un estado para almacenar el resultado y los indicadores relacionados con la carga y los mensajes de error.
Reemplaza el contenido de App.js
con el siguiente código:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
export default class App extends React.Component {
state = {
users: [],
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.setState({ isLoading: true });
axios
.get('https://randomuser.me/api/?page=0&results=10')
.then((response) => {
this.setState({ users: response.data.results, errorMsg: '' });
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
}
render() {
const { users, isLoading, errorMsg } = this.state;
console.log(users);
return (
<div className="main-section">
<Header />
{isLoading && <p className="loading">Loading...</p>}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
</div>
);
}
}
Aquí, hemos declarado un estado directamente dentro de la clase usando la sintaxis de propiedades de la clase, que es una forma común de escribir el estado en componentes basados en clases.
state = {
users: [],
isLoading: false,
errorMsg: ''
};
Luego, dentro del método componentDidMount
primero establecemos el estado isLoading
en true
antes de realizar la llamada a la API.
this.setState({ isLoading: true });
Una vez que obtenemos la respuesta del API, almacenamos el resultado en la matriz de users
que se declara en el estado. También estamos configurando el estado de errorMsg
en vacío, de modo que si hay algún error anterior, se borrará.
this.setState({ users: response.data.results, errorMsg: '' });
Y en el bloque .catch
, estamos configurando el errorMsg
en caso de que haya algún error al realizar una llamada a la API.
Luego, usamos el bloque .finally
para establecer el estado isLoading
en false
.
.finally(() => {
this.setState({ isLoading: false });
});
Usar finally
nos ayuda a evitar la duplicación de código aquí porque no necesitamos establecer isLoading
a false
en .then
y en el bloque .catch
nuevamente. Esto se debe a que el bloque finally
se ejecutará siempre, tenga éxito o no.
Y en el método render
, mostramos el mensaje de error o el mensaje de carga junto con la matriz de users
desde el estado en la consola.
Ahora, si verificas la aplicación, verás la información de users
en la consola sobre el éxito o un mensaje de error en la UI por falla del API.
Cómo mostrar la información de los usuarios
Ahora, mostremos la información users
en la pantalla.
Crea un nuevo archivo User.js
dentro de la carpeta components
con el siguiente contenido:
import React from "react";
const User = ({ name, location, email, picture }) => {
return (
<div className="random-user">
<div className="user-image">
<img src={picture.medium} alt={name.first} />
</div>
<div className="user-details">
<div>
<strong>Name:</strong> {name.first} {name.last}
</div>
<div>
<strong>Country:</strong> {location.country}
</div>
<div>
<strong>Email:</strong> {email}
</div>
</div>
</div>
);
};
export default User;
Ahora, crea un nuevo archivo UsersList.js
dentro de la carpeta components
con el siguiente contenido:
import React from 'react';
import User from './User';
const UsersList = ({ users }) => {
return (
<div className="user-list">
{users && users.map((user) => <User key={user.login.uuid} {...user} />)}
</div>
);
};
export default UsersList;
Ahora, abre el archivo App.js
y reemplaza el método render
con el siguiente código:
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
{isLoading && <p className="loading">Loading...</p>}
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<UsersList users={users} />
</div>
);
}
Aquí, estamos pasando el arreglo de users
como propiedades al componente UsersList
. Dentro del componente UsersList
, estamos recorriendo el arreglo y enviando la información del usuario al componente User
mediante la distribución de todas las propiedades del user
individual como {...props}
. Esto finalmente muestra los datos en la pantalla.
Además, importa el componente UsersList
en la parte superior del archivo:
import UsersList from './components/UsersList';
Si revisas la aplicación ahora, verás la siguiente pantalla:
Como puedes ver, en cada actualización de página, se muestra un nuevo conjunto de usuarios aleatorios en la pantalla.
Cómo implementar funcionalidad de cargar más
Ahora, agreguemos la funcionalidad de cargar más que permitirá a nuestra aplicación cargar el siguiente conjunto de 10 usuarios con cada clic de carga.
Cambia el método render
del archivo App.js
al siguiente código:
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
Aquí, hemos agregado la condición isLoading
dentro del botón para mostrar el texto Loading...
o Load More
en el botón.
Agrega la nueva propiedad page
al estado e inicialízala en 0
.
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
Y agrega la función loadMore
antes del método render
para incrementar el valor del estado de page
en 1 en cada clic de botón.
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
Aquí, estamos usando el estado anterior para calcular el siguiente valor de estado page
, por lo que el código anterior es el mismo que el código siguiente:
loadMore = () => {
this.setState((prevState) => {
return {
page: prevState.page + 1
};
});
};
Solo estamos usando la sintaxis abreviada de ES6 para devolver un objeto de la función.
Ahora, dentro del método componentDidMount
, cambie la URL del API con el código:
'https://randomuser.me/api/?page=0&results=10'
a este código:
`https://randomuser.me/api/?page=${page}&results=10`
Aquí, estamos usando la sintaxis de plantilla literal ES6 para usar el valor dinámico del estado de page
para cargar el siguiente conjunto de usuarios en cada clic de botón.
Desestructuramos page
desde el estado dentro del método componentDidMount
de esta manera:
componentDidMount() {
const { page } = this.state;
....
}
Ahora, revisemos la funcionalidad de la aplicación.
Como puedes ver, cuando hacemos clic en el botón Load More
, el estado de page
está cambiando en las herramientas de desarrollo de react, pero no aparece la nueva lista de usuarios en la pantalla.
Esto se debe a que, aunque estamos cambiando el estado de page
, no volvemos a realizar una llamada al API para obtener el siguiente grupo de usuarios con el valor de page
modificado. Así que arreglemos esto.
Crea una nueva función loadUsers
encima de la función loadMore
y mueve todo el código de componentDidMount
al interior de la función loadUsers
. Luego llama a la función loadUsers
desde el método componentDidMount
.
También, agrega el método componentDidUpdate
dentro del componente de la aplicación como este:
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
Como estamos actualizando el valor del estado de page
en la función loadMore
una vez que se actualiza el estado, se llamará al método componentDidUpdate
. Entonces, estamos verificando si el valor del estado anterior de page
no es igual al valor del estado actual. Luego volvemos a hacer la llamada al API llamando a la función loadUsers
.
Revisa el siguiente artículo para aprender más sobre por qué y cuándo necesitamos usar el método componentDidUpdate
.
Tu archivo completo App.js
se verá así ahora:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
import UsersList from './components/UsersList';
export default class App extends React.Component {
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.loadUsers();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
loadUsers = () => {
const { page } = this.state;
this.setState({ isLoading: true });
axios
.get(`https://randomuser.me/api/?page=${page}&results=10`)
.then((response) => {
this.setState({ users: response.data.results, errorMsg: '' });
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
};
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
}
Ahora, si vuelves a comprobar la aplicación ejecutando el comando yarn start
, verás la siguiente pantalla:
Como puedes ver, estamos obteniendo una nueva lista de usuarios que se muestra en cada clic de botón de carga. Pero el problema es que solo podemos ver a 10 usuarios a la vez.
Así que hagamos cambios para agregar nuevos usuarios a la lista de usuarios que ya se muestra.
Para esto, necesitamos cambiar la forma en que configuramos el estado de users
.
Nuestra llamada setState
actual dentro de la función loadUsers
se ve así:
this.setState({ users: response.data.results, errorMsg: '' });
Aquí, siempre reemplazamos el arreglo de users
con el nuevo conjunto de usuarios. Así que cambie la llamada setState
anterior al siguiente código:
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
Aquí, estamos usando la sintaxis del actualizador de setState
. Estamos creando un nuevo arreglo distribuyendo los users
ya agregados usando ...prevState.users
, y luego agregamos un nuevo conjunto de users
usando ..response.data.results
.
De esta manera, no perderemos los datos de los users
cargados anteriormente y también podremos agregar un nuevo conjunto de users
.
Ahora, si vuelves a comprobar la aplicación, verás el comportamiento correcto de la carga de datos.
Cómo mejorar el código usando Async/await
Si revisas la función loadUsers
, verás que el código parece complejo y difícil de leer.
loadUsers = () => {
const { page } = this.state;
this.setState({ isLoading: true });
axios
.get(`https://randomuser.me/api/?page=${page}&results=10`)
.then((response) => {
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
})
.catch((error) =>
this.setState({
errorMsg: 'Error while loading data. Try again later.'
})
)
.finally(() => {
this.setState({ isLoading: false });
});
};
Podemos arreglar esto usando la sintaxis async/await.
Primero, necesitamos marcar la función loadUsers
como asíncrona:
loadUsers = async () => {
Porque podemos usar la palabra clave await
solo dentro de la función que se declara como async
.
Ahora, reemplaza la función loadUsers
con el siguiente código:
loadUsers = async () => {
try {
const { page } = this.state;
this.setState({ isLoading: true });
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
} catch (error) {
this.setState({
errorMsg: 'Error while loading data. Try again later.'
});
} finally {
this.setState({ isLoading: false });
}
};
Aquí, hemos usado la palabra clave await
antes de la llamada axios.get
, por lo que la siguiente línea de código, que es la llamada setState
, no se ejecutará hasta que obtengamos la respuesta del API.
Si hay algún error al obtener la respuesta del API, se ejecutará el bloque catch
. El bloque finally
establecerá el estado isLoading
en false
.
Tu archivo App.js
modificado se verá así ahora:
import React from 'react';
import Header from './components/Header';
import axios from 'axios';
import UsersList from './components/UsersList';
export default class App extends React.Component {
state = {
users: [],
page: 0,
isLoading: false,
errorMsg: ''
};
componentDidMount() {
this.loadUsers();
}
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
this.loadUsers();
}
}
loadUsers = async () => {
try {
const { page } = this.state;
this.setState({ isLoading: true });
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
this.setState((prevState) => ({
users: [...prevState.users, ...response.data.results],
errorMsg: ''
}));
} catch (error) {
this.setState({
errorMsg: 'Error while loading data. Try again later.'
});
} finally {
this.setState({ isLoading: false });
}
};
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
render() {
const { users, isLoading, errorMsg } = this.state;
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={this.loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
}
}
Ahora, el código de la función loadUsers
parece mucho más limpio y más fácil de entender que antes. Y si revisas la aplicación, verás que también está funcionando correctamente.
Cómo refactorizar el código de componente de clase en código de componente funcional
Hemos terminado de desarrollar la funcionalidad completa de la aplicación. Así que refactoricemos el código para usar componentes funcionales con React Hooks.
Si eres nuevo en React Hooks, revisa el siguiente articulo para una introducción a los Hooks.
Crea un nuevo archivo llamado AppFunctional.js
dentro de la carpeta src
con el siguiente contenido:
import React from 'react';
const AppFunctional = () => {
return (
<div>
<h2>Functional Component</h2>
</div>
);
};
export default AppFunctional;
Hemos creado un nuevo archivo para el componente funcional para que pueda comparar el código y guardarlo para su referencia.
Ahora, abre el archivo index.js
y reemplaza el contenido del archivo con el siguiente código:
import React from 'react';
import ReactDOM from 'react-dom';
import AppFunctional from './AppFunctional';
import './styles.css';
ReactDOM.render(<AppFunctional />, document.getElementById('root'));
Aquí, hemos utilizado el componente AppFunctional
dentro del método render
y también hemos agregado la importación para él mismo en la parte superior del archivo.
Ahora, si vuelves a arrancar tu aplicación usando el comando yarn start
, verás la siguiente pantalla:
Entonces, estamos mostrando correctamente el código del componente AppFunctional
en la pantalla.
Ahora, reemplace el contenido del componente AppFunctional
con el siguiente código:
import React, { useState, useEffect } from 'react';
import axios from 'axios';
import Header from './components/Header';
import UsersList from './components/UsersList';
const AppFunctional = () => {
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
useEffect(() => {
const loadUsers = async () => {
try {
setIsLoading(true);
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
setUsers([...users, ...response.data.results]);
setErrorMsg('');
} catch (error) {
setErrorMsg('Error while loading data. Try again later.');
} finally {
setIsLoading(false);
}
};
loadUsers();
}, []);
const loadMore = () => {
setPage((page) => page + 1);
};
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
};
export default AppFunctional;
Aquí, inicialmente declaramos los estados requeridos usando el hook useState
:
const [users, setUsers] = useState([]);
const [page, setPage] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [errorMsg, setErrorMsg] = useState('');
Luego agregamos un hook useEffect
y le pasamos un arreglo vacío []
como segundo argumento. Esto significa que el código dentro del hook useEffect
se ejecutará solo una vez cuando se monte el componente.
useEffect(() => {
// your code
}, []);
Hemos trasladado toda la función loadUsers
dentro del hook useEffect
y luego la llamamos dentro del gancho de esta manera:
useEffect(() => {
const loadUsers = async () => {
// your code
};
loadUsers();
}, []);
También hemos eliminado todas las referencias a this.state
ya que los componentes funcionales no necesitan el contexto this
.
Antes de realizar la llamada al API, establecemos el estado isLoading
en true
usando setIsLoading(true);
.
Como ya tenemos acceso al arreglo de users
dentro del componente, estamos configurando directamente como una nuevo arreglo para la función setUsers
de esta manera:
setUsers([...users, ...response.data.results]);
Si quieres saber porqué no podemos usar la palabra claveasync
directamente en la función hookuseEffect
, revisa el siguiente articulo.
Luego, hemos cambiado la función loadMore
de éste código:
loadMore = () => {
this.setState((prevState) => ({
page: prevState.page + 1
}));
};
a éste código:
const loadMore = () => {
setPage((page) => page + 1);
};
Tenga en cuenta que para declarar una función basada en componentes funcionales, debe agregarconst
olet
antes de la declaración. Como la función no va a cambiar, se recomienda usarconst
por ejemploconst loadMore = () => {}
.
Luego, copiamos el contenido del método render
tal como está dentro del componente AppFunctional
para devolver el JSX. También hemos cambiado onClick = {this.loadMore}
a onClick = {loadMore}
.
return (
<div className="main-section">
<Header />
<UsersList users={users} />
{errorMsg && <p className="errorMsg">{errorMsg}</p>}
<div className="load-more">
<button onClick={loadMore} className="btn-grad">
{isLoading ? 'Loading...' : 'Load More'}
</button>
</div>
</div>
);
Ahora, si revisas la aplicación, verás la siguiente pantalla:
Como puedes ver, los usuarios se están cargando correctamente, pero la funcionalidad de carga más no funciona.
Esto se debe a que solo estamos haciendo la llamada al API una vez cuando el componente está montado, ya que estamos pasando el arreglo de dependencia vacía []
como segundo argumento para el hook useEffect
.
Para hacer la llamada al API nuevamente cuando cambia el estado de page
, necesitamos agregar la page
como una dependencia para el hook useEffect
como este:
useEffect(() => {
// execute the code to load users
}, [page]);
El useEffect
anterior es lo mismo que escribir el siguiente código:
componentDidUpdate(prevProps, prevState) {
if (prevState.page !== this.state.page) {
// execute the code to load users
}
}
useEffect
hace que sea realmente fácil escribir menos código que sea fácil de entender.
Entonces, ahora con este cambio, el código dentro del hook useEffect
se ejecutará cuando el componente se monte y cuando se cambie el estado page
.
Ahora, si revisas la aplicación, verás que la funcionalidad de carga más está funcionando nuevamente como se esperaba.
Pero si verificas la terminal/símbolo del sistema, es posible que observes una advertencia como se muestra a continuación (si tienes ESLint
instalado en su máquina):
Estas advertencias nos ayudan a evitar problemas en nuestra aplicación que podrían ocurrir más adelante, por lo que siempre es bueno solucionarlos si es posible.
Como estamos haciendo referencia al estado de users
dentro de la función loadUsers
, debemos incluirlo también en en arreglo de dependencia. Así que hagámoslo.
Incluye a los users
como una dependencia junto con page
como se muestra a continuación:
useEffect(() => {
// your code
}, [page, users]);
Comprobemos ahora la funcionalidad de la aplicación.
Como puedes ver, continuamente estamos obteniendo un nuevo conjunto de usuarios a medida que nos desplazamos por la página y la aplicación avanza en un bucle infinito.
Esto se debe a que cuando se monta el componente, el código dentro del hook useEffect
se ejecutará para realizar una llamada al API. Una vez que obtenemos el resultado, configuramos el arreglo de users
.
Como los users
son mencionados en la lista de dependencias, una vez que se cambia el arreglo de usuarios, useEffect
se ejecutará nuevamente y sucederá una y otra vez creando un bucle infinito.
Entonces, para solucionar esto, debemos evitar hacer referencia al arreglo de users
externos de alguna manera. Para hacer eso, usemos la sintaxis del actualizador de setState
para configurar el estado de users
.
Por lo tanto, cambia el siguiente código:
setUsers([...users, ...response.data.results]);
a este código:
setUsers((users) => [...users, ...response.data.results]);
Aquí, estamos usando el valor anterior de users
para crear un nuevo arreglo de users
.
Ahora, podemos eliminar los users
del arreglo de dependencias de useEffect
ya que no estamos haciendo referencia a la variable externa users
.
Tu hook useEffect
modificado se verá así ahora:
useEffect(() => {
const loadUsers = async () => {
try {
setIsLoading(true);
const response = await axios.get(
`https://randomuser.me/api/?page=${page}&results=10`
);
setUsers((users) => [...users, ...response.data.results]);
setErrorMsg('');
} catch (error) {
setErrorMsg('Error while loading data. Try again later.');
} finally {
setIsLoading(false);
}
};
loadUsers();
}, [page]);
Si revisas la aplicación ahora, verás funciona como se esperaba sin ningún problema.
Y ahora tampoco estamos recibiendo ningún error en la terminal.
¡Gracias por leer!
Puedes encontrar el código fuente completo para esta aplicación en este repositorio y una demostración en vivo de la aplicación implementada aquí.
Traducido del artículo de Yogesh Chavan - How to Build a React Application with Load More Functionality using React Hooks