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 12 días
Mezclar demasiadas responsabilidades dentro de un mismo componente puede ocasionar que con el tiempo sea difícil de escalar y mantener.
Un ejemplo muy común ocurre con los formularios de creación y edición. Generalmente reutilizamos el mismo formulario para evitar duplicar código, pero poco a poco comenzamos a agregar condiciones: si estamos creando ejecuta una petición, si estamos editando ejecuta otra, transforma los datos de cierta forma, etc.
Aunque la interfaz puede ser la misma, crear y editar representan procesos diferentes.
En este artículo analizaremos un caso práctico donde un componente maneja ambas responsabilidades y veremos cómo separarlas para crear formularios más escalables, fáciles de probar y mantener.
Cuando trabajamos con formularios es muy común pensar que crear y editar un registro son prácticamente lo mismo.
Después de todo, ambos tienen los mismos campos:
Entonces reutilizar el formulario parece la decisión más obvia. El problema aparece cuando hacemos que ese mismo formulario también sea responsable de controlar todo el proceso.
Un flujo de creación puede necesitar realizar acciones como:
Mientras que un flujo de edición puede tener necesidades diferentes:
Estos son solo algunos ejemplos, pero conforme nuestra aplicación crece también aparecen reglas de negocio: campos que dependen de otros campos, permisos, validaciones especiales o comportamientos específicos dependiendo del contexto.
El objetivo no es dejar de reutilizar formularios. El objetivo es evitar que nuestro formulario termine con responsabilidades que no le pertenecen.
Para este caso práctico no utilizaremos librerías externas, estilos CSS, hooks personalizados ni configuraciones adicionales.
La idea es enfocarnos únicamente en el problema principal: un formulario que empieza a manejar más responsabilidades de las que debería.
Partiremos de un componente sencillo que permite crear y editar un usuario desde el mismo componente.
import { useEffect, useState } from "react";
export default function UserForm({ id }) {
const isEditing = Boolean(id);
const [data, setData] = useState({
name: "",
email: "",
});
const [notification, setNotification] = useState("");
useEffect(() => {
if (!isEditing) return;
fetch(`/api/users/${id}`)
.then((response) => response.json())
.then((user) => {
setData({
name: user.name,
email: user.email,
});
});
}, [id, isEditing]);
function handleChange(event) {
setData({
...data,
[event.target.name]: event.target.value,
});
}
function handleSubmit(event) {
event.preventDefault();
const payload = {
name: data.name,
email: data.email,
};
if (isEditing) {
fetch(`/api/users/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then(() => {
setNotification("La edición se realizó correctamente");
});
return;
}
fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
}).then(() => {
setNotification("La creación se realizó correctamente");
});
}
return (
<div>
<h1>{isEditing ? "Editar usuario" : "Crear usuario"}</h1>
{notification && <p>{notification}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>Nombre</label>
<input
type="text"
name="name"
value={data.name}
onChange={handleChange}
/>
</div>
<div>
<label>Correo</label>
<input
type="email"
name="email"
value={data.email}
onChange={handleChange}
/>
</div>
<button type="submit">
{isEditing ? "Guardar cambios" : "Crear usuario"}
</button>
</form>
</div>
);
}
Aunque nuestro componente funciona correctamente, poco a poco empieza a tener demasiadas responsabilidades.
Algunos problemas que podemos encontrar son:
La solución no es crear dos formularios diferentes y duplicar código. La solución es separar las responsabilidades y dejar que el formulario se encargue únicamente de manejar los datos que el usuario captura.
La separación de responsabilidades es importante tanto en la vida diaria como en el desarrollo de software. Cuando cada parte tiene claro qué debe hacer, es más fácil modificar, probar y mantener nuestro código.
En el ejemplo anterior, el formulario sabe demasiado: decide si está creando o editando, consulta información, prepara el payload, llama diferentes endpoints y muestra mensajes según el flujo. Para mejorar esto, vamos a separar esas responsabilidades en componentes más pequeños, donde cada uno tenga un propósito claro.
import { useEffect, useState } from "react";
function FormUser({ initialValues, onSubmit }) {
const [data, setData] = useState({
name: initialValues?.name || "",
email: initialValues?.email || "",
});
const [notification, setNotification] = useState("");
useEffect(() => {
setData({
name: initialValues?.name || "",
email: initialValues?.email || "",
});
}, [initialValues]);
function handleChange(event) {
setData({
...data,
[event.target.name]: event.target.value,
});
}
async function handleSubmit(event) {
event.preventDefault();
const response = await onSubmit(data);
setNotification(response.message);
}
return (
<div>
{notification && <p>{notification}</p>}
<form onSubmit={handleSubmit}>
<div>
<label>Nombre</label>
<input
type="text"
name="name"
value={data.name}
onChange={handleChange}
/>
</div>
<div>
<label>Correo</label>
<input
type="email"
name="email"
value={data.email}
onChange={handleChange}
/>
</div>
<button type="submit">Guardar</button>
</form>
</div>
);
}
export function NewUser() {
async function handleCreate(data) {
const payload = {
name: data.name,
email: data.email,
};
await fetch("/api/users", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return {
message: "La creación se realizó correctamente",
};
}
return (
<div>
<h1>Crear usuario</h1>
<FormUser onSubmit={handleCreate} />
</div>
);
}
export function EditUser({ id }) {
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${id}`)
.then((response) => response.json())
.then((user) => {
setUser(user);
});
}, [id]);
async function handleUpdate(data) {
const payload = {
name: data.name,
email: data.email,
};
await fetch(`/api/users/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
return {
message: "La edición se realizó correctamente",
};
}
if (!user) {
return <p>Cargando usuario...</p>;
}
return (
<div>
<h1>Editar usuario</h1>
<FormUser initialValues={user} onSubmit={handleUpdate} />
</div>
);
}
Con esta separación cada componente tiene una responsabilidad más clara:
No buscamos crear más componentes simplemente por separar código. Buscamos que cada pieza tenga una responsabilidad clara y que los cambios futuros sean más fáciles de realizar.
Los formularios son una parte fundamental dentro de nuestras aplicaciones frontend. Mantener una buena separación de responsabilidades nos permite crear componentes más escalables, fáciles de mantener y modificar.
Muchas veces no necesitamos separar funcionalidades desde el inicio. Conforme nuestra aplicación crece, el mismo componente comienza a mostrarnos señales de que está manejando demasiadas responsabilidades y es momento de adaptarlo.
Si quieres aprender más sobre cómo crear componentes escalables, fáciles de modificar y mantener con ayuda de pruebas automatizadas, te recomiendo mi hub de Testing en React.
Aprende cómo el testing puede ayudarte a diseñar mejores componentes y desarrollar aplicaciones React con más confianza.
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.