Open OmarAfet opened 8 months ago
HTML input elements, including file input, <input type=" file">
, accept a value prop. These value props represent a selected file. For security purposes in the browser, setting the value
is not allowed. Therefore, we cannot directly set value
props to a 'File 'object or array. Here, react-hook-form
manages the form state and file uploads. And also handles the value
input field itself. It's expecting undefined
initially, allowing the user to select files via the browser file picker. By setting value={undefined}
we can resolve this. This will indicate to react-hook-form
that the field is initially empty. And may avoid the error you are encountering.
defaultValues: {
images: [undefined], // Set initial value to undefined instead of new File([], '')
},
<Input
{...field}
multiple
type="file"
accept="image/jpeg, image/png"
value={undefined} // Set value to undefined for file input field
/>
Source -> https://stackoverflow.com/questions/72557334/react-hook-form-and-setvalue-of-file-input
Thanks @Nilesh-Meena , this fixes the TypeScript error. but now when I upload an image(s), a validation error occurs saying Expected array, received string
We can initialize image
with an empty array []
rather than a single element in an array with undefined
. I hope this works.
We can initialize
image
with an empty array[]
rather than a single element in an array withundefined
. I hope this works.
It doesn't work. The problem isn't with the initial value; it's about Zod schema validation.
export const schema = z.object({
images: z.array(
z
.instanceof(File)
.refine((file) => file.size <= MAX_UPLOAD_SIZE, "Image size must be less than 1MB.")
.refine((file) => ACCEPTED_FILE_TYPES.includes(file.type), "Invalid file type. Only JPEG and
PNG are allowed.")
),
});
I think the issue is input
component is passing a string value to the image
field. which is expected to be an array according to Zod schema. I have updated both the Zod schema and the Input
props to resolve this. Edit the code however you like.
"use client";
import { Input } from "@/components/ui/input";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { string, z } from "zod";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Button } from "@/components/ui/button";
const MAX_UPLOAD_SIZE = 1024 * 1024 * 1; // 1MB
const ACCEPTED_FILE_TYPES = ["image/jpeg", "image/png"];
const schema = z.object({
images: z
.array(
z
.any()
.refine(
(file) => file?.size <= MAX_UPLOAD_SIZE,
`Max image size is 1 MB.`
)
.refine(
(file) => ACCEPTED_FILE_TYPES.includes(file?.type),
"Only.jpeg, .png formats are supported."
)
)
.default([]), // Default value for array field
});
export default function Home() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
});
const [validationErrors, setValidationErrors] = useState<string[]>([]);
async function onSubmit(formData: z.infer<typeof schema>) {
try {
// Check for oversized files
const oversizedFiles = formData.images.filter(
(file) => file.size > MAX_UPLOAD_SIZE
);
if (oversizedFiles.length > 0) {
setValidationErrors([`Max image size is 1 MB.`]);
return;
}
// If no oversized files
await schema.parseAsync(formData);
console.log("Form data submitted:", formData);
} catch (error) {
if (error instanceof Error) {
console.error("Validation failed:", error.message);
} else {
console.error("An unexpected error occurred:", error);
}
}
}
return (
<main>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="images"
render={({ field }) => (
<FormItem>
<FormLabel>images</FormLabel>
<FormControl>
<Input
{...field}
multiple
type="file"
accept="image/jpeg, image/png"
value={undefined}
onChange={(e) => {
const files = e.target.files;
if (files) {
const fileList = Array.from(files);
const oversizedFiles = fileList.filter(
(file) => file.size > MAX_UPLOAD_SIZE
);
if (oversizedFiles.length > 0) {
setValidationErrors([`Max image size is 1MB.`]);
} else {
setValidationErrors([]);
field.onChange(fileList);
}
}
}}
/>
</FormControl>
<FormMessage>{validationErrors.join(", ")}</FormMessage>
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</main>
);
}
When No file is selected: image array is empty
when a single file is selected: image array shows 1 file
when multiple file is selected: image array shows multiple files
When file size exceeds:
Max image size is 1MB.
Currently, we are not storing previously added images to the array. if you want this functionality you can do this.
Thank you for your code. I've made some changes to improve type safety and added default values for the form fields. But, I'm facing an issue where the custom error messages from the Zod schema aren't displaying correctly. Instead, I'm getting undefined
I noticed that you've created a state for error messages, but I believe this isn't the ideal solution. Zod should handle that, and we need to fix it.
Check out the code here with the new changes I've made. Try to submit without files and with files more than 1MB. It will show undefined
. I'm not sure why this is happening, but I suspect it might be due to value={undefined}
.
z.array(...)
part, the undefined error was replaced with actual errors.you are right, Zod should handle the validation. I have updated the code. Previously I was experimenting with the code and trying to find where the error Expected array, received string
came from. And then I forgot to remove the states for the error message.
This updated code checks both file size and file type within a single .refine()
call. if both checks pass, Zod considers the validation field to have passed, and no error message is returned. And we update the formData, as you can see in the attached images.
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import * as z from 'zod';
const MAX_FILE_SIZE = 1024 * 1024 * 1;
const ACCEPTED_IMAGE_EXTENSIONS = ['jpeg', 'jpg', 'png'];
const formSchema = z.object({
adImage: z.array(z.any()).refine(
(files) => {
// Check if any file exceeds the maximum size
const maxSizeExceeded = files.some(
(file) => file?.file?.size > MAX_FILE_SIZE
);
if (maxSizeExceeded) {
return false;
}
// Check if all files have an accepted file extension
const invalidFileExtension = files.some((file) => {
const fileNameParts = file?.file?.name.split('.');
const fileExtension = fileNameParts[fileNameParts.length - 1];
return !ACCEPTED_IMAGE_EXTENSIONS.includes(fileExtension.toLowerCase());
});
return !invalidFileExtension;
},
{
message: 'File exceeds the maximum size, or unsupported file extension.',
}
),
});
export default function MyForm() {
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
adImage: undefined,
},
});
const onSubmit = (formData: z.infer<typeof formSchema>) => {
console.log('button clicked');
console.log('Form data:', formData.adImage);
};
const handleFileSelection = (e: any, field: any) => {
const files = e.target.files;
if (files && files.length > 0) {
const fileList = Array.from(files); // Convert FileList to array
const validatedFiles = fileList.map((file) => ({
file,
})); // Validate each file
field.onChange(validatedFiles); // Update form field value
}
};
return (
<main>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="adImage"
render={({ field }) => (
<FormItem>
<FormLabel>Images</FormLabel>
<FormControl>
<Input
{...field}
multiple
type="file"
accept="image/jpeg, image/png"
value={undefined}
onChange={(e) => handleFileSelection(e, field)}
/>
</FormControl>
{/* Display form error message */}
{/* <FormMessage>
{formState.errors.adImage?.message || ''}
</FormMessage> */}
</FormItem>
)}
/>
<FormMessage>{form.formState.errors.adImage?.message}</FormMessage>
<Button type="submit">Submit</Button>
</form>
</Form>
</main>
);
}
When No image is selected:
When Invalid file type is selected:
when the File exceeds the limit:
When Everything checks out we should be able to add the image:
Thanks @Nilesh-Meena for your code.
The problem with the error message undefined
was with the z.array()
function. Therefore, adding .refine()
after z.array()
instead of within it solves the issue.
I don't know what the issue is related to... Shadcn, react-hook-form, or zod, so I'll keep it open for @shadcn to view it before closing it.
For anyone wanting a Shadcn input file that accepts multiple files with Zod validation and react-hook-form, here's a simple form:
"use client";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
const MAX_UPLOAD_SIZE = 1024 * 1024 * 1; // 1MB
const ACCEPTED_FILE_TYPES = ["image/jpeg", "image/png"];
const schema = z.object({
images: z
.array(z.instanceof(File))
.min(1, "You must upload at least 1 image.")
.max(3, "You can upload a maximum of 3 images.")
.refine((files) => files.every((file) => file.size <= MAX_UPLOAD_SIZE && ACCEPTED_FILE_TYPES.includes(file.type)), "All images must be less than 1MB and of type JPEG or PNG."),
});
export default function Home() {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
defaultValues: {
images: [new File([], "")],
},
});
function onSubmit(values: z.infer<typeof schema>) {
console.log(values);
}
return (
<main>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="images"
render={({ field }) => (
<FormItem>
<FormLabel>Images</FormLabel>
<FormControl>
<Input
{...field}
multiple
type="file"
accept="image/jpeg, image/png"
value={undefined}
onChange={(e) => {
const files = e.target.files;
if (files) {
field.onChange(Array.from(files));
}
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button className="w-full" type="submit">
Submit
</Button>
</form>
</Form>
</main>
);
}
The issue still seems to be present; I can't send a form with that file input.
the solution of setting undefined in defaultvalues and also in the input value={undefined} works fine in fields where user can write. But it doesn't work in readOnly input. This input is modified automatically in a useEffect using form.setValue("pricePerUnit", result). it sets the value only when I remove the undefined values in defaultvalues and value={undefined}
still an issue right?
The core problem lies in how the Input
component handles the value
prop when dealing with file inputs. When using an <input type="file">
, the value
attribute is read-only in browsers due to security reasons; it can only be programmatically set to an empty string (''
or undefined
), not to an array of File
objects or any other value.
In this case, the react-hook-form
's field
object includes a value
property, which when spread into the Input
component (<Input {...field} />
), causes the value
prop to be set to a File[]
. This leads to the TypeScript error you're seeing, as well as runtime errors when the browser tries to handle the value
of the file input.
So, instead of spreading the entire field
object into the Input
component, extract only the necessary properties and omit value
.
// The `field` object includes the `value` property, which should be omitted
<FormField
control={form.control}
name="images"
render={({ field }) => (
<FormItem>
<FormLabel>Images</FormLabel>
<FormControl>
<Input
name={field.name}
ref={field.ref}
onBlur={field.onBlur}
onChange={(e) => {
const files = e.target.files;
if (files) {
// Convert FileList to an array and update form state
field.onChange(Array.from(files));
}
}}
multiple
type="file"
accept="image/jpeg, image/png"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
But there's one more thing to consider: you can't pass instances of the File
class directly to a Server Action because Next.js only supports serializable data types like plain objects, arrays, strings, numbers, and a few built-in types. They do not support class instances like File
.
To work around this limitation, you can use a FormData
object to send the files to the server. Here's an example of how you can do this:
async function onSubmit(values: z.infer<typeof schema>) {
const formData = new FormData();
values.images.forEach((file) => {
formData.append("images", file);
});
await serverAction(formData); // Send the form data to the server
}
Also, don't forget to add the encType="multipart/form-data"
attribute to the <form>
tag to ensure that file data is correctly encoded and transmitted to the server.
Thank you for your code. I've made some changes to improve type safety and added default values for the form fields. But, I'm facing an issue where the custom error messages from the Zod schema aren't displaying correctly. Instead, I'm getting
undefined
I noticed that you've created a state for error messages, but I believe this isn't the ideal solution. Zod should handle that, and we need to fix it.
Check out the code here with the new changes I've made. Try to submit without files and with files more than 1MB. It will show
undefined
. I'm not sure why this is happening, but I suspect it might be due tovalue={undefined}
.EDIT: After removing the
z.array(...)
part, the undefined error was replaced with actual errors.
i have a question: What will be the data type of the image input if I have to use it in another component? Will it be an array or a File?e?
Describe the bug
I'm encountering a TypeScript error in my NextJS app when trying to use an
Input
component for file uploads. The error message is as follows:The error occurs in the following code snippet:
The
Input
component is being used with thereact-hook-form
library and thezod
schema validation library. The relevant part of the schema is:The
Input
component'svalue
prop is expected to be astring | number | readonly string[] | undefined
, but it's receiving aFile[]
due to theimages
field in the form.Any help resolving this issue would be greatly appreciated.
Affected component/components
Input
How to reproduce
app/page.tsx
add this code:import { Input } from '@/components/ui/input'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form';
const MAX_UPLOAD_SIZE = 1024 1024 1; // 1MB const ACCEPTED_FILE_TYPES = ['image/jpeg', 'image/png'];
const schema = z.object({ images: z.array( z .instanceof(File) .refine( (file) => file.size <= MAX_UPLOAD_SIZE, 'Image size must be less than 1MB.' ) .refine( (file) => ACCEPTED_FILE_TYPES.includes(file.type), 'Invalid file type. Only JPEG and PNG are allowed.' ) ), });
export default function Home() { const form = useForm<z.infer>({
resolver: zodResolver(schema),
defaultValues: {
images: [new File([], '')],
},
});
function onSubmit(values: z.infer) {
console.log(values);
}
return (
); }
System Info
Before submitting