I am using Nextjs and Resend to send emails from different forms. All my forms send the data without problems except for one which involves uploading a file.
This is how I build my forms.
I have a FormGroup component:
"use client";
import { useEffect, useState } from "react";
type FormGroupProps = {
label?: string;
inputType?: "input" | "textarea" | "select" | "file";
inputProps?:
| React.InputHTMLAttributes<HTMLInputElement>
| React.TextareaHTMLAttributes<HTMLTextAreaElement>;
selectOptions?: Array<{ value: string; label: string }>;
error?: string;
};
export default function FormGroup({
label,
inputType = "input",
inputProps,
selectOptions = [],
error,
}: FormGroupProps) {
const [hasError, setHasError] = useState(false);
useEffect(() => {
setHasError(error !== undefined);
}, [error]);
return (
<div
className={`form-group flex flex-col h-full gap-2.5 ${
inputProps?.className || ""
}`}
>
{label && (
<label
className="text-xs md:text-sm text-[#393E4F]"
htmlFor={inputProps?.id}
>
{label}
</label>
)}
{inputType === "input" && (
<input
type="text"
className={`border ${
hasError ? "border-red-500" : "border-gray-300"
} rounded w-full px-2 py-2 md:px-4 focus:outline-none focus:border-lightblue-primary`}
{...(inputProps as React.InputHTMLAttributes<HTMLInputElement>)}
/>
)}
{error && <div className="text-red-500 text-[10px] mt-1">{error}</div>}
{inputType === "textarea" && (
<textarea
className="border border-gray-300 rounded w-full px-2 py-2 md:px-4 h-24 focus:outline-none focus:border-lightblue-primary"
{...(inputProps as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
/>
)}
{inputType === "file" && (
<input
type="file"
className="border border-gray-300 rounded w-full px-2 py-2 md:px-4 focus:outline-none focus:border-lightblue-primary"
{...(inputProps as React.InputHTMLAttributes<HTMLInputElement>)}
/>
)}
{inputType === "select" && (
<div className="flex flex-wrap gap-2">
{selectOptions?.map((option) => (
<label key={option.value} className="flex items-center gap-2">
<input
type="radio"
value={option.value}
name={inputProps?.name}
onChange={inputProps?.onChange}
className="form-radio"
/>
{option.label}
</label>
))}
</div>
)}
</div>
);
}
Then I have a GenericForm component:
"use client";
import React, { useState } from "react";
import FormGroup from "./FormGroup";
import FormTitle from "./FormTitle";
interface GenericFormProps<T extends FormData> {
title: string;
fields: Array<{ label: string; inputType: string; name: keyof T }>;
onSubmit: (data: T) => void;
errors: { [key in keyof T]?: string };
handleChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
interface FormData {
nombre: string;
apellido: string;
dni: string;
celular: string;
mail: string;
comentarios: string;
cv?: {
name: string;
type: string;
base64: string;
};
}
function GenericForm<T extends FormData>({
title,
fields,
onSubmit,
errors,
handleChange,
}: GenericFormProps<T>) {
const initialFormData = {} as T;
const [formData, setFormData] = useState<T>(initialFormData);
const combinedHandleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (handleChange) {
handleChange(event);
}
const { name, value, files } = event.target as HTMLInputElement & {
files: FileList | null;
};
setFormData((prevData) => ({
...prevData,
[name]: files ? files[0] : value,
}));
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
onSubmit(formData);
if (!Object.values(errors).some((error) => error !== undefined)) {
setFormData(initialFormData);
}
};
return (
<>
<FormTitle title={title} />
<div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 form-container">
{fields.map((field, index) => (
<div
key={index}
className={`${
fields.length % 2 !== 0 && index === fields.length - 1
? "md:col-span-2"
: ""
}`}
>
<FormGroup
label={field.label}
inputType={field.inputType}
inputProps={{
value: formData[field.name] || "",
onChange: combinedHandleChange,
name: field.name,
}}
selectOptions={field.selectOptions}
error={errors[field.name]}
/>
</div>
))}
</div>
<div className="flex justify-center mt-4">
<button
type="submit"
onClick={handleSubmit}
className="bg-[#00ADEE] w-full text-white py-2 px-8 rounded-[8px] focus:outline-none"
>
Enviar
</button>
</div>
</div>
</>
);
}
export default GenericForm;
And the form that is failing is TrabajaConNosotros.tsx:
"use client";
import GenericForm from "./GenericForm";
import TrabajaConNosotrosSchema from "@/schemes/trabaja-con-nosotros.scheme";
import handleSubmit from "@/utils/submitForm";
import { useState } from "react";
const TrabajaConNosotros = () => {
const fields = [
{ label: "Nombre", inputType: "input", name: "nombre" },
{ label: "Apellido", inputType: "input", name: "apellido" },
{ label: "DNI (sin puntos)", inputType: "input", name: "dni" },
{ label: "Celular", inputType: "input", name: "celular" },
{ label: "Mail", inputType: "input", name: "mail" },
{ label: "Comentarios", inputType: "textarea", name: "comentarios" },
{ label: "Adjuntar CV", inputType: "file", name: "cv" },
];
const [errors, setErrors] = useState({});
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
setSelectedFile(file);
}
};
const readFileAsync = (file: File): Promise<string> =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result as string);
reader.onerror = reject;
reader.readAsDataURL(file);
});
const onSubmit = async (data: FormData) => {
if (selectedFile) {
try {
const base64 = await readFileAsync(selectedFile);
const formDataWithCV = {
...data,
cv: {
name: selectedFile.name,
type: selectedFile.type,
base64: base64.split(',')[1],
},
};
await handleSubmit(formDataWithCV, TrabajaConNosotrosSchema, 'trabajar_con_nosotros');
setErrors({});
} catch (err) {
console.error(err);
}
} else {
try {
await handleSubmit(data, TrabajaConNosotrosSchema, 'trabajar_con_nosotros');
setErrors({});
} catch (err) {
if (err.inner) {
const formErrors = err.inner.reduce((acc, currentError) => {
acc[currentError.path] = currentError.message;
return acc;
}, {});
setErrors(formErrors);
} else {
setErrors({ general: "Ha ocurrido un error desconocido" });
}
}
}
};
return (
<GenericForm
title="Trabajá con nosotros"
fields={fields}
onSubmit={onSubmit}
errors={errors}
handleChange={handleChange}
/>
);
};
export default TrabajaConNosotros;
The error occurs when I select a file to upload and inmediately have the following error in my screen:
Uncaught DOMException: An attempt was made to use an object that is not, or is no longer, usable
From this error message I assume it has something to do with the onChange event but I have no clue how to fix it.