Compartir en Twitter
Go to Homepage

CÓMO LIMPIAR FORMULARIOS DINÁMICOS EN REACT DE FORMA EFICIENTE

November 1, 2025

Introducción al manejo de formularios dinámicos en aplicaciones React

El desarrollo de aplicaciones web modernas con React frecuentemente involucra la creación de formularios complejos y dinámicos. Estos formularios pueden contener múltiples campos agrupados por categorías, tipos de datos o funcionalidades específicas. Uno de los desafíos más comunes surge al intentar limpiar todos los valores ingresados por el usuario tras realizar una acción como enviar el formulario o restablecer el estado inicial. Aunque es posible manipular directamente el DOM para vaciar los inputs, esta práctica va en contra de los principios fundamentales de React y puede generar problemas de mantenibilidad y rendimiento.

En este tutorial profundo exploraremos técnicas robustas y profesionales para implementar la limpieza completa de formularios dinámicos, manteniendo el control total del estado de la aplicación. Analizaremos tanto enfoques con clases como soluciones modernas con hooks funcionales, priorizando siempre las mejores prácticas recomendadas por la comunidad React en 2025.

Estructura inicial de un formulario dinámico con datos agrupados

Para comprender el problema, primero necesitamos establecer un caso de uso real. Imaginemos una aplicación de configuración donde los usuarios definen parámetros agrupados por categorías. Cada grupo contiene múltiples campos que deben mantenerse sincronizados en el estado de la aplicación.

interface Item {
    name: string;
    description: string;
    group: string;
    dtype: string;
}

interface AppState {
    items: Item[];
    itemvalues: Array<Record<string, Array<Record<string, string[]>>>>;
}

La estructura del estado itemvalues es particularmente compleja porque agrupa los valores por group, y dentro de cada grupo mantiene un array de objetos donde cada objeto representa los valores de un campo específico. Esta arquitectura permite manejar múltiples entradas por campo (separadas por comas) y mantener la relación entre el nombre del campo y sus valores.

Problemas comunes al intentar limpiar formularios dinámicos

Muchos desarrolladores, al enfrentarse por primera vez con este desafío, recurren a soluciones rápidas como seleccionar todos los inputs del DOM y establecer su propiedad value en cadena vacía:

const inputs = document.querySelectorAll("input");
Array.from(inputs).forEach((input) => (input.value = ""));

Si bien esta técnica limpia visualmente los campos, presenta múltiples problemas críticos:

  • Viola el principio de unidirectional data flow de React
  • Crea inconsistencias entre el DOM real y el virtual de React
  • Puede causar comportamientos impredecibles en renderizados posteriores
  • Dificulta el testing y el debugging
  • No escala bien en aplicaciones grandes

Solución profesional: componentes controlados con estado centralizado

La solución correcta implica mantener todos los inputs como componentes controlados, donde el valor del input siempre refleja el estado de la aplicación. Esto nos permite restablecer todos los campos simplemente actualizando el estado.

Implementación con clases (legada pero ilustrativa)

class ConfigApp extends React.Component<{}, AppState> {
    constructor(props: {}) {
        super(props);
        this.state = {
            items: [
                {
                    name: "server",
                    description: "Servidor principal",
                    group: "infra",
                    dtype: "str",
                },
                {
                    name: "port",
                    description: "Puerto de conexión",
                    group: "infra",
                    dtype: "str",
                },
                {
                    name: "theme",
                    description: "Tema visual",
                    group: "ui",
                    dtype: "str",
                },
                {
                    name: "language",
                    description: "Idioma",
                    group: "ui",
                    dtype: "str",
                },
            ],
            itemvalues: [{}],
        };
    }

    handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        const { dataset, name, value } = e.target;
        const group = dataset.group!;

        this.setState((prevState) => {
            const newValues = { ...prevState.itemvalues[0] };

            if (!newValues[group]) {
                newValues[group] = [];
            }

            const fieldIndex = newValues[group].findIndex(
                (item) => item[name] !== undefined
            );
            const processedValues = value
                .split(",")
                .map((v) => v.trim())
                .filter((v) => v);

            if (fieldIndex === -1) {
                newValues[group].push({ [name]: processedValues });
            } else {
                newValues[group][fieldIndex][name] = processedValues;
            }

            return { itemvalues: [newValues] };
        });
    };

    handleReset = () => {
        this.setState({ itemvalues: [{}] });
    };

    handleSubmit = () => {
        console.log("Valores actuales:", this.state.itemvalues);
    };

    render() {
        return (
            <ConfigForm
                items={this.state.items}
                values={this.state.itemvalues[0]}
                onChange={this.handleChange}
                onSubmit={this.handleSubmit}
                onReset={this.handleReset}
            />
        );
    }
}

Componente de formulario con inputs controlados

interface ConfigFormProps {
    items: Item[];
    values: Record<string, Array<Record<string, string[]>>>;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
    onSubmit: () => void;
    onReset: () => void;
}

const ConfigForm: React.FC<ConfigFormProps> = ({
    items,
    values,
    onChange,
    onSubmit,
    onReset,
}) => {
    const getFieldValue = (item: Item): string => {
        const group = values[item.group];
        if (!group) return "";

        const field = group.find((f) => f[item.name]);
        return field ? field[item.name].join(", ") : "";
    };

    return (
        <div className="config-form">
            {items.map((item, index) => (
                <div key={index} className="form-field">
                    <label>{item.description}</label>
                    <input
                        type="text"
                        name={item.name}
                        placeholder={item.description}
                        data-group={item.group}
                        value={getFieldValue(item)}
                        onChange={onChange}
                    />
                </div>
            ))}

            <div className="form-actions">
                <button type="button" onClick={onSubmit}>
                    Enviar configuración
                </button>
                <button type="button" onClick={onReset}>
                    Limpiar todo
                </button>
            </div>
        </div>
    );
};

Enfoque moderno con React Hooks y TypeScript

En 2025, el uso de hooks funcionales con TypeScript es el estándar de facto. Veamos una implementación más elegante:

interface FormItem {
    id: string;
    label: string;
    group: string;
    placeholder: string;
}

const useDynamicForm = (initialItems: FormItem[]) => {
    const [values, setValues] = useState<
        Record<string, Record<string, string[]>>
    >({});

    const updateField = useCallback(
        (group: string, name: string, value: string) => {
            setValues((prev) => {
                const newGroup = { ...(prev[group] || {}) };
                const processed = value
                    .split(",")
                    .map((v) => v.trim())
                    .filter(Boolean);

                if (processed.length === 0) {
                    const { [name]: _, ...rest } = newGroup;
                    return { ...prev, [group]: rest };
                }

                return { ...prev, [group]: { ...newGroup, [name]: processed } };
            });
        },
        []
    );

    const reset = useCallback(() => {
        setValues({});
    }, []);

    const getValue = useCallback(
        (group: string, name: string): string => {
            const groupValues = values[group];
            if (!groupValues || !groupValues[name]) return "";
            return groupValues[name].join(", ");
        },
        [values]
    );

    return { values, updateField, reset, getValue };
};

Componente funcional principal

const DynamicConfigForm: React.FC<{ items: FormItem[] }> = ({ items }) => {
    const { getValue, updateField, reset, values } = useDynamicForm(items);

    const groupedItems = useMemo(() => {
        return items.reduce((acc, item) => {
            if (!acc[item.group]) acc[item.group] = [];
            acc[item.group].push(item);
            return acc;
        }, {} as Record<string, FormItem[]>);
    }, [items]);

    const handleSubmit = (e: React.FormEvent) => {
        e.preventDefault();
        console.log("Formulario enviado:", values);
    };

    return (
        <form onSubmit={handleSubmit} className="dynamic-form">
            {Object.entries(groupedItems).map(([group, groupItems]) => (
                <fieldset key={group} className="form-group">
                    <legend>
                        {group.charAt(0).toUpperCase() + group.slice(1)}
                    </legend>
                    {groupItems.map((item) => (
                        <div key={item.id} className="form-control">
                            <label htmlFor={item.id}>{item.label}</label>
                            <input
                                id={item.id}
                                type="text"
                                placeholder={item.placeholder}
                                value={getValue(group, item.id)}
                                onChange={(e) =>
                                    updateField(group, item.id, e.target.value)
                                }
                            />
                        </div>
                    ))}
                </fieldset>
            ))}

            <div className="form-footer">
                <button type="submit">Guardar configuración</button>
                <button type="button" onClick={reset}>
                    Restablecer formulario
                </button>
            </div>
        </form>
    );
};

Ventajas del enfoque con componentes controlados

La implementación con componentes controlados ofrece múltiples beneficios:

  • Consistencia absoluta entre el estado de la aplicación y la interfaz
  • Facilidad para implementar validaciones complejas
  • Posibilidad de deshacer/rehacer acciones
  • Mejor soporte para testing con React Testing Library
  • Integración natural con formularios de terceros como React Hook Form

Integración con React Hook Form para formularios complejos

Para aplicaciones empresariales, React Hook Form ofrece una solución optimizada:

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";

const formSchema = z.object({
    infra: z.object({
        server: z.array(z.string()).min(1),
        port: z.array(z.string()).min(1),
    }),
    ui: z.object({
        theme: z.array(z.string()).min(1),
        language: z.array(z.string()).min(1),
    }),
});

type FormData = z.infer<typeof formSchema>;

const AdvancedForm: React.FC = () => {
    const { control, handleSubmit, reset, watch } = useForm<FormData>({
        resolver: zodResolver(formSchema),
        defaultValues: {
            infra: { server: [], port: [] },
            ui: { theme: [], language: [] },
        },
    });

    const onSubmit = (data: FormData) => {
        console.log("Datos validados:", data);
    };

    const handleReset = () => {
        reset({
            infra: { server: [], port: [] },
            ui: { theme: [], language: [] },
        });
    };

    return (
        <form onSubmit={handleSubmit(onSubmit)}>
            {/* Controllers para cada campo */}
            <button type="button" onClick={handleReset}>
                Limpiar formulario
            </button>
        </form>
    );
};

Patrones avanzados de limpieza selectiva

En aplicaciones reales, frecuentemente necesitamos limpiar solo ciertos grupos o campos:

const useSelectiveReset = () => {
    const [values, setValues] = useState<Record<string, any>>({});

    const resetGroup = (group: string) => {
        setValues((prev) => {
            const { [group]: _, ...rest } = prev;
            return rest;
        });
    };

    const resetField = (group: string, field: string) => {
        setValues((prev) => ({
            ...prev,
            [group]: {
                ...(prev[group] || {}),
                [field]: [],
            },
        }));
    };

    return {
        values,
        setValues,
        resetGroup,
        resetField,
        resetAll: () => setValues({}),
    };
};

Manejo de formularios con referencias (refs) cuando es necesario

Aunque no es la práctica recomendada, en ciertos casos legacy podemos combinar refs con estado:

const LegacyForm: React.FC = () => {
    const inputRefs = useRef<Record<string, HTMLInputElement>>({});
    const [values, setValues] = useState<Record<string, string>>({});

    const registerRef = (name: string) => (el: HTMLInputElement) => {
        if (el) inputRefs.current[name] = el;
    };

    const clearAll = () => {
        Object.values(inputRefs.current).forEach((input) => {
            input.value = "";
        });
        setValues({});
    };

    return (
        <div>
            <input ref={registerRef("email")} />
            <button onClick={clearAll}>Limpiar con refs</button>
        </div>
    );
};

Buenas prácticas para 2025

  1. Siempre prefiera componentes controlados sobre no controlados
  2. Utilice TypeScript para tipar correctamente sus formularios
  3. Implemente validación en el momento adecuado (onChange vs onSubmit)
  4. Considere bibliotecas especializadas para formularios complejos
  5. Mantenga la lógica de formulario separada de la lógica de negocio
  6. Implemente patrones de accesibilidad desde el inicio

Ejemplo completo integrado

const CompleteDynamicForm: React.FC = () => {
    const [formData, setFormData] = useState<
        Record<string, Record<string, string[]>>
    >({});

    const updateValue = (group: string, field: string, value: string) => {
        const values = value
            .split(",")
            .map((v) => v.trim())
            .filter(Boolean);

        setFormData((prev) => {
            if (values.length === 0) {
                const newGroup = { ...prev[group] };
                delete newGroup[field];
                const newData = { ...prev };
                if (Object.keys(newGroup).length === 0) {
                    delete newData[group];
                } else {
                    newData[group] = newGroup;
                }
                return newData;
            }

            return {
                ...prev,
                [group]: {
                    ...(prev[group] || {}),
                    [field]: values,
                },
            };
        });
    };

    const getFieldValue = (group: string, field: string): string => {
        return formData[group]?.[field]?.join(", ") || "";
    };

    const resetForm = () => setFormData({});

    const items = [
        { id: "db-host", label: "Host BD", group: "database" },
        { id: "db-port", label: "Puerto BD", group: "database" },
        { id: "api-url", label: "URL API", group: "backend" },
        { id: "timeout", label: "Timeout", group: "backend" },
    ];

    return (
        <form
            onSubmit={(e) => {
                e.preventDefault();
                console.log(formData);
            }}
        >
            {items.map((item) => (
                <div key={item.id}>
                    <label>{item.label}</label>
                    <input
                        value={getFieldValue(item.group, item.id)}
                        onChange={(e) =>
                            updateValue(item.group, item.id, e.target.value)
                        }
                        placeholder={item.label}
                    />
                </div>
            ))}
            <button type="submit">Enviar</button>
            <button type="button" onClick={resetForm}>
                Reset completo
            </button>
        </form>
    );
};

Conclusiones

El manejo profesional de formularios dinámicos en React requiere un entendimiento profundo del flujo de datos unidireccional y el uso apropiado del estado de la aplicación. Aunque las soluciones rápidas con manipulación directa del DOM pueden parecer atractivas por su simplicidad, generan problemas técnicos a largo plazo que afectan la mantenibilidad del código.

Las mejores prácticas actuales recomiendan fuertemente el uso de componentes controlados, donde el estado de React es la única fuente de verdad para los valores de los inputs. Esta aproximación no solo resuelve el problema de limpieza de formularios de manera elegante, sino que habilita características avanzadas como validación en tiempo real, deshacer/rehacer, y testing automatizado.

Para aplicaciones modernas en 2025, combine hooks personalizados con TypeScript y considere bibliotecas especializadas como React Hook Form para escenarios complejos. La inversión inicial en una arquitectura sólida de formularios se traduce en código más robusto, mantenible y preparado para escalar con las necesidades del proyecto.