Cómo consumir APIs en React sin ensuciar tus componentes

Gabriel Jiménez | Hace alrededor de 1 hora

Cuando trabajamos con APIs en nuestras aplicaciones frontend es muy común tomar la información devuelta por el backend y utilizarla directamente dentro de nuestros componentes.


Al inicio parece una solución simple: recibimos un JSON, guardamos los datos en un estado y los mostramos en pantalla. Sin embargo, conforme nuestra aplicación crece, nuestros componentes comienzan a depender demasiado de la estructura del backend. Pequeños cambios como renombrar una propiedad, cambiar un formato de fecha o modificar una relación pueden terminar afectando diferentes partes de nuestra interfaz.


En este artículo aprenderemos cómo transformar los datos que recibimos y enviamos al backend para crear componentes React más fáciles de mantener, modificar y probar.


Ejemplo práctico: Cómo consumir un API en React correctamente

Para entender cómo consumir un API correctamente en React, iremos mejorando un formulario paso a paso.


Primero crearemos una implementación sencilla utilizando estados independientes para cada campo. Después veremos los problemas que aparecen cuando el formato que utiliza nuestro frontend comienza a ser diferente al formato esperado por el backend.


Finalmente agregaremos la funcionalidad de editar información existente y refactorizaremos nuestro componente para separar la forma en que recibimos y enviamos datos al backend.


Creando nuestro primer formulario conectado al backend

Cuando creamos un formulario en React que necesita comunicarse con un backend, normalmente comenzamos con una implementación sencilla: tenemos algunos campos, almacenamos sus valores en estados y al hacer clic en guardar enviamos la información al servidor.


Veamos un ejemplo básico de un formulario que permite registrar un usuario:


function UserForm() {
  const [name, setName] = useState("");
  const [lastname, setLastname] = useState("");
  const [birthday, setBirthday] = useState("");

  async function saveUser() {
    await fetch("/users", {
      method: "POST",
      body: JSON.stringify({
        name,
        lastname,
        birthday,
      }),
    });
  }

  return (
    <>
      <input 
        value={name}
        onChange={e => setName(e.target.value)}
      />

      <input 
        value={lastname}
        onChange={e => setLastname(e.target.value)}
      />

      <button onClick={saveUser}>
        Guardar
      </button>
    </>
  );
}

A simple vista nuestro componente funciona correctamente: tenemos nuestros estados, enviamos la información al backend y podemos crear usuarios.


Sin embargo, nuestro componente acaba de tomar una responsabilidad extra: conocer cómo necesita recibir los datos del backend.


Frontend y backend tienen formatos diferentes

Uno de los problemas más comunes al comunicar nuestro frontend con el backend es encontrarnos con que ambos manejan estructuras de datos diferentes.


La mayoría de las veces, el equipo encargado del frontend y el equipo encargado del backend no son las mismas personas. Cada equipo toma decisiones dependiendo de sus necesidades: nombres de propiedades, formatos de fechas, estructuras de objetos, etc.


Como desarrolladores frontend, debemos separar aquello que podemos controlar de aquello que no. No siempre tendremos control sobre cómo un API devuelve o recibe la información, pero sí podemos controlar cómo nuestros componentes trabajan con esos datos. Por eso, en lugar de adaptar nuestros componentes al formato del backend, podemos crear una capa encargada de transformar la información entre nuestra interfaz y el API.


Por ejemplo, nuestro backend podría necesitar recibir el siguiente payload:


{
  "first_name": "Gabriel",
  "last_name": "Jiménez",
  "birth_date": "1995-10-20"
}

Pero tu formulario trabaja con:


{
  name: "Gabriel",
  lastname: "Jiménez",
  birthday: "1995-10-20"
}

Entonces, terminamos con algo así:


body: JSON.stringify({
  first_name: name,
  last_name: lastname,
  birth_date: birthday,
})

Pero el problema no aparece únicamente cuando enviamos información al backend. ¿Qué sucede cuando necesitamos hacer el proceso contrario?


Supongamos que nuestro formulario ahora permite editar un usuario existente. Para llenar los campos necesitamos consumir el API y transformar la respuesta del backend al formato que nuestro formulario necesita.


Probablemente terminaríamos con algo parecido a esto:


useEffect(() => {
  fetch("/users/1")
    .then(response => response.json())
    .then(user => {
      setName(user.first_name);
      setLastname(user.last_name);
      setBirthday(user.birth_date);
    });
}, []);

Nuestro componente ahora conoce dos cosas:


  • Cómo convertir datos para enviarlos.
  • Cómo convertir datos cuando llegan.
  • Cada cambio en el API puede terminar modificando nuestra interfaz.

Separando nuestro formulario del backend

Una buena forma de crear componentes más fáciles de mantener es separar responsabilidades. Nuestro formulario debería encargarse de manejar la interacción del usuario, no de conocer todos los detalles sobre cómo el backend recibe o devuelve información.


En este caso, podemos separar dos responsabilidades:


  • Preparar los datos antes de enviarlos al backend.
  • Transformar la respuesta del backend al formato que necesita nuestro formulario.

Para lograrlo podemos utilizar funciones encargadas de adaptar la información, normalmente conocidas como adapters o mappers.


Veamos un ejemplo:


Primero mejoras el estado


const [form, setForm] = useState({
  name: "",
  lastname: "",
  birthday: "",
});

Introducción de adapters/mappers


function userFromApi(user) {
  return {
    name: user.first_name,
    lastname: user.last_name,
    birthday: user.birth_date,
  };
}

y:


function userToApi(form) {
  return {
    first_name: form.name,
    last_name: form.lastname,
    birth_date: form.birthday,
  };
}

Componente con adapters / mappers


import { useEffect, useState } from "react";

function userFromApi(user) {
  return {
    name: user.first_name,
    lastname: user.last_name,
    birthday: user.birth_date,
  };
}

function userToApi(form) {
  return {
    first_name: form.name,
    last_name: form.lastname,
    birth_date: form.birthday,
  };
}

export default function UserForm() {
  const [form, setForm] = useState({
    name: "",
    lastname: "",
    birthday: "",
  });

  useEffect(() => {
    async function loadUser() {
      const response = await fetch("/users/1");
      const user = await response.json();

      setForm(userFromApi(user));
    }

    loadUser();
  }, []);

  function handleChange(event) {
    setForm({
      ...form,
      [event.target.name]: event.target.value,
    });
  }

  async function saveUser() {
    await fetch("/users/1", {
      method: "PUT",
      body: JSON.stringify(
        userToApi(form)
      ),
    });
  }

  return (
    <>
      <input
        name="name"
        value={form.name}
        onChange={handleChange}
      />

      <input
        name="lastname"
        value={form.lastname}
        onChange={handleChange}
      />

      <input
        name="birthday"
        value={form.birthday}
        onChange={handleChange}
      />

      <button onClick={saveUser}>
        Guardar
      </button>
    </>
  );
}

Ahora nuestro componente dejó de depender directamente del backend. Si mañana cambia first_name por firstName, o la fecha llega en otro formato, nuestro formulario no necesita modificarse.


Únicamente actualizamos nuestros adapters o mappers y mantenemos nuestros componentes enfocados en su verdadera responsabilidad: manejar la interfaz del usuario.


Conclusión

Proteger nuestros componentes de factores externos que no podemos controlar, como la estructura utilizada por el backend, nos ayuda a crear aplicaciones más fáciles de mantener y escalar con el tiempo.


Separar responsabilidades permite que nuestros componentes se enfoquen en la interfaz, mientras otras partes de nuestra aplicación se encargan de transformar y preparar la información.


Si quieres aprender más técnicas para crear componentes fáciles de modificar, escalar y probar, te recomiendo mi hub sobre Testing en React.


Hub Testing en React.

Más de esta serie

Cómo organizar tus carpetas y archivos en React para escalar tu aplicación

Cómo organizar tus carpetas y archivos en React para escalar tu aplicación

Aprende a organizar carpetas y archivos en React para crear proyectos más mantenibles, escalables y fáciles de desarrollar.

¿Cuándo usar hooks personalizados en React (y cuándo no)?

¿Cuándo usar hooks personalizados en React (y cuándo no)?

Aprende cuándo crear hooks personalizados en React para reutilizar lógica, simplificar componentes y mejorar el testing.

Por qué tu componente en React es difícil de probar (y cómo solucionarlo)

Por qué tu componente en React es difícil de probar (y cómo solucionarlo)

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.

Señales de un componente difícil de mantener en React

Señales de un componente difícil de mantener en React

Aprende a detectar componentes difíciles de mantener en React antes de que se conviertan en deuda técnica y errores costosos.