Cómo consumir APIs en React sin ensuciar tus componentes
Aprende cómo transformar datos entre React y tu backend usando adapters y mappers para crear componentes más fáciles de mantener y escalar.
Gabriel Jiménez | Hace 11 días
Trabajar correctamente los errores del backend en nuestras aplicaciones React, facilita
la retroalimentación al usuario sobre las acciones que debe hacer. Sin una gestión correcta de errores, el usuario puede sentirse perdido y puede abandonar nuestra aplicación.
En esté artículo, vamos a crear un proyecto que:
Al final, tendrás la capacidad de interpretar si el código generado maneja los errores de la mejor forma.
Los contratos permiten definir estructuras de datos de una forma sencilla. Las estructuras deben de tener un sindicado claro y fácil de entender por los involucrados.
Por ejemplo: La siguiente estructura es payload devuelto en caso de error en algún campo.
{
"errors": [
{
"field": "email",
"message": "El correo electrónico ya está registrado"
},
{
"field": "password",
"message": "La contraseña debe tener al menos 8 caracteres"
},
{
"field": "name",
"message": “Campo obligatorio"
}
]
}
Si lo analizamos es bastante sencillo determinar que hace. La propiedad “errors” explícitamente dice que contiene una lista de errores producidos, donde cada error especifica el campo y el tipo de error ocurrido.
Generalmente, quien debería definir este contrato son: tanto el equipo de backend como frontend. Sin embargo, en la practica a veces no sucede, ya sea porque somos nosotros mismo que cumplimos ambos roles o por el poco tiempo que se tiene para planear.
Lo importante a la hora de definir nuestros payload es que, debe tener una estructura clara, un significado fácil de entender y ser consistente a través de toda la aplicación. Si usas una estructura para mostrar errores de validación de campos, esa estructura debe aplicarse en cualquier parte de la aplicación que valide campos.
Los errores de validación son aquellos donde se valida los campos previo a ser procesados, mientras que los errores de lógica son aquellos que dependen de todo un contexto.
Para entender mejor la diferente entre cada uno, veamos un ejemplo:
Errores de Validación
El ejemplo más común es el que vemos al enviar un formulario vació. Obtenemos errores como: Campo obligatorio, Debe tener 10 caracteres mínimo, etc.
Errores de lógica
Estamos agendado una cita y al momento de elegir la fecha, resulta que ya no hay espacio con el doctor “x” dentro de la sucursal “y” dentro del horario “z”, por que alguien más ya la reservo.
Tener bien separado este tipo de errores, nos va a permitir tener claro que errores son de validación y cuales de lógica de negocio.
Al ya tener identificados los tipos de errores devueltos por nuestro backend, ahora hay que decidir donde mostrarlos.
En el caso de los errores de validación conviene mostrarlos directamente en cada uno de los campos. Para el caso de los de negocio, tal vez conviene mostrarlos al inicio o la final.
Al tener claro donde posicionar cada tipo de error, ayudamos al usuario a identificar un patrón de errores en nuestra aplicación.
Cuando centralizamos la gestión de errores estamos definiendo una forma de como se deben de trabajar los errores en nuestra aplicación. Esto facilita el probar, localizar y modificar sin miedo a romper algo ya existente.
No existe la mejor forma de centralizar tus errores y es lo bueno de la programación que, para un problema existen muchas soluciones y cada persona tiene su propio criterio. Sin embargo, siempre mantén presente la centralización de las cosas.
Nuestras aplicaciones están en constante cambio, por lo que posiblemente, el código se vea afectado, ya sea por un cambio nuevo o una mejora. Dicho esto, debemos validar que lo que exista hoy, siga funcionando como se debe.
Por ejemplo:
Supongamos que, agregamos una nueva funcionalidad al mismo componente que se encarga de validar los campos. A la hora de probar manualmente en nuestro navegador, tendremos que asegurarnos que la nueva funcionalidad no afecte la validación de campos.
Esto puede ir subiendo de complejidad al agregar nuevas cosas. Para asegurarnos no romper algo ya desarrollado en nuestras aplicaciones frontend, las pruebas unitarias, de integración y end to end son el mejor aliado.
El ejemplo práctico consiste en crear un formulario de registro, donde el usuario puede ingresar su nombre, correo y contraseña.
Pruebas automatizadas para evitar regresiones
import {render, screen, fireEvent} from "@testing-library/react";
import RegisterForm from "./RegisterForm";
test("muestra errores por campo cuando falla la validación del backend", async () => {
// Definición del payload de error, devuelto por el backend
const registerUser = jest.fn().mockRejectedValue({
response: {
errors: [
{
field: "email",
message: "El correo electrónico ya está registrado",
},
{
field: "password",
message: "La contraseña debe tener al menos 8 caracteres",
},
{
field: "name",
message: "Campo obligatorio",
},
],
},
});
render(<RegisterForm registerUser={registerUser}/>);
fireEvent.change(screen.getByLabelText("Nombre"), {
target: {value: ""},
});
fireEvent.change(screen.getByLabelText("Correo"), {
target: {value: "[email protected]"},
});
fireEvent.change(screen.getByLabelText("Contraseña"), {
target: {value: "123"},
});
fireEvent.click(screen.getByText("Registrarme"));
expect(
await screen.findByText("El correo electrónico ya está registrado")
).toBeInTheDocument();
expect(
screen.getByText("La contraseña debe tener al menos 8 caracteres")
).toBeInTheDocument();
expect(screen.getByText("Campo obligatorio")).toBeInTheDocument();
});
test("muestra error general cuando el correo ya existe", async () => {
// Definición del payload de error, devuelto por el backend
const registerUser = jest.fn().mockRejectedValue({
response: {
message: "El correo [email protected], ya se encuentra registrado",
},
});
render(<RegisterForm registerUser={registerUser}/>);
fireEvent.change(screen.getByLabelText("Nombre"), {
target: {value: "Juanito"},
});
fireEvent.change(screen.getByLabelText("Correo"), {
target: {value: "[email protected]"},
});
fireEvent.change(screen.getByLabelText("Contraseña"), {
target: {value: "password123"},
});
fireEvent.click(screen.getByText("Registrarme"));
expect(
await screen.findByText(
"El correo [email protected], ya se encuentra registrado"
)
).toBeInTheDocument();
});
Componente productivo
import {useState} from "react";
export default function RegisterForm({registerUser}) {
const [data, setData] = useState({
name: "",
email: "",
password: "",
});
// Separación por tipo de error: Errores de validación y negocio
const [fieldErrors, setFieldErrors] = useState({});
const [generalError, setGeneralError] = useState("");
function handleFormErrors({
error,
setFieldErrors,
setGeneralError,
}) {
const response = error.response;
if (response?.errors) {
const errorsByField = {};
response.errors.forEach((item) => {
errorsByField[item.field] = item.message;
});
setFieldErrors(errorsByField);
return;
}
if (response?.message) {
setGeneralError(response.message);
}
}
const handleChange = (event) => {
const {name, value} = event.target;
setData({
...data,
[name]: value,
});
};
const handleSubmit = async (event) => {
event.preventDefault();
setFieldErrors({});
setGeneralError("");
try {
await registerUser(data);
} catch (error) {
// Centralizar la gestión de errores
handleFormErrors({
error,
setFieldErrors,
setGeneralError,
});
}
};
return (
<form onSubmit={handleSubmit}>
{
// Errores de negocio
}
{generalError && <p role="alert">{generalError}</p>}
<div>
<label htmlFor="name">Nombre</label>
<input
id="name"
name="name"
value={data.name}
onChange={handleChange}
/>
{
// Errores de validación
fieldErrors.name && <p role="alert">{fieldErrors.name}</p>
}
</div>
<div>
<label htmlFor="email">Correo</label>
<input
id="email"
name="email"
value={data.email}
onChange={handleChange}
/>
{
// Errores de validación
fieldErrors.email && <p role="alert">{fieldErrors.email}</p>
}
</div>
<div>
<label htmlFor="password">Contraseña</label>
<input
id="password"
name="password"
type="password"
value={data.password}
onChange={handleChange}
/>
{
// Errores de validación
fieldErrors.password && <p role="alert">{fieldErrors.password}</p>
}
</div>
<button type="submit">Registrarme</button>
</form>
)
;
}
Desarrollar escribiendo el código poco a poco va ir desapareciendo, pero las estrategias que tomemos para desarrollar mejor código dependerán de nuestro criterio.
Si quieres aprender más sobre Testing en React y cómo evitar regresiones en tu código, tengo un hub dedicado a ello.
Aprende cómo transformar datos entre React y tu backend usando adapters y mappers para crear componentes más fáciles de mantener y escalar.
Aprende a organizar carpetas y archivos en React para crear proyectos más mantenibles, escalables y fáciles de desarrollar.
Aprende cuándo crear hooks personalizados en React para reutilizar lógica, simplificar componentes y mejorar el testing.
Aprende por qué algunos componentes en React son difíciles de probar y cómo organizarlos para crear código más escalable y mantenible.