Cómo organizar formularios de creación y edición en React sin complicar tus componentes

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.


Crear y editar parecen lo mismo, pero no lo son

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:


  • Nombre
  • Correo
  • Teléfono

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:


  • Enviar información a un endpoint de creación.
  • Preparar los datos en cierto formato antes de enviarlos.
  • Redireccionar después de guardar.
  • Mostrar una notificación de registro creado correctamente.

Mientras que un flujo de edición puede tener necesidades diferentes:


  • Consultar la información existente.
  • Adaptar la respuesta del backend al formato del formulario.
  • Comparar o enviar únicamente los campos modificados.
  • Consumir un endpoint diferente.
  • Mostrar una notificación de actualización correcta.

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.


Caso práctico: un formulario con demasiadas responsabilidades

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:


  • El formulario conoce demasiados detalles del backend. El componente sabe qué endpoint llamar, qué método HTTP utilizar y cómo debe enviarse la información.
  • La lógica de creación y edición está mezclada. Cada nueva diferencia entre crear y editar agrega más condiciones dentro del mismo componente.
  • Es más difícil modificar un flujo sin afectar el otro. Un cambio necesario para la edición podría romper accidentalmente la creación, aunque sean procesos diferentes.
  • Las transformaciones de información viven dentro del formulario. Si el backend cambia el formato esperado, tenemos que modificar directamente nuestro componente visual.
  • La carga de información está acoplada al formulario. El formulario necesita saber cuándo consultar información, de dónde obtenerla y cómo adaptarla.
  • Las reglas de negocio empiezan a mezclarse con la interfaz. Validaciones, permisos o comportamientos especiales terminan viviendo junto con el JSX.
  • Es más complicado escribir pruebas. Para probar el formulario necesitamos considerar creación, edición, llamadas al backend, transformaciones y diferentes respuestas.

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.


Separando las responsabilidades del formulario

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:


  • FormUser se encarga únicamente del formulario. Ahora el formulario no sabe si está creando o editando un usuario. Su responsabilidad es manejar los campos, capturar la información y enviar los datos mediante la función onSubmit.
  • NewUser controla el proceso de creación. Toda la lógica relacionada con crear un usuario vive en este componente: preparar la información, llamar al endpoint correspondiente y definir qué sucede después de guardar.
  • EditUser controla el proceso de edición. La edición tiene sus propias necesidades, como consultar la información existente, inicializar el formulario y enviar los cambios al endpoint correcto.
  • El formulario ya no depende del backend. FormUser no necesita conocer URLs, métodos HTTP o estructuras necesarias para comunicarse con el servidor. Solo entrega la información capturada.
  • Eliminar condiciones innecesarias. Ya no necesitamos preguntar constantemente si estamos creando o editando (isEditing). Cada componente representa un flujo específico.
  • Modificar un flujo es más seguro. Si mañana cambia la manera de crear usuarios, podemos modificar NewUser sin afectar la edición. Lo mismo ocurre si cambia la lógica de actualización.
  • Las pruebas son más sencillas. Podemos probar el formulario validando únicamente su comportamiento, y probar creación o edición de manera independiente.

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.


Conclusión

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.


Hub Testing en React.

Más de esta serie

Cómo consumir APIs en React sin ensuciar tus componentes

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.

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.