I am building a file upload system in my React app where users can upload videos, images, or subtitles. The files are uploaded using a custom <AdminFileUpload />
component that generates a preview of the selected file using URL.createObjectURL()
. However, when I switch tabs and come back to the file upload section, the file preview disappears, but the file URL still exists in the React state.
What I Want:
πCurrent Behavior (Bug): When I upload a file (video, image, or subtitle), it instantly shows a preview. After switching tabs (from Show Details to Show Upload) and returning back, the preview vanishes, but the file URL is still in the state (UploadDetails). If I click upload again, the form still contains the correct file URL β itβs just the preview that disappears.
I only know this much about my problem also i know that useCallback
will get used but i dont know how to
so im giving all my inter related files so please help me fix it
Problem in video:: GDRIVE
Full code: Code
ShowUpload.jsx
import { useEffect, useState } from "react";
import { Separator } from "@/components/ui/separator";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ShowBasicDetails } from "./tabs/ShowBasicDetails";
import { ShowFileUpload } from "./tabs/showFileUpload";
import { ScrollArea } from "../../ui/scroll-area";
import { Button } from "../../ui/button";
import { toast } from "react-hot-toast";
import {
FINAL_initialState,
FINAL_showVideoInitialFormData,
} from "../../../config/formFields";
export const ShowUpload = () => {
const [activeTab, setActiveTab] = useState("showDetails");
const [showDetailsData, setShowDetailsData] = useState(FINAL_initialState);
const [UploadDetailsData, setUploadDetailsData] = useState({});
const [category, setCategory] = useState("");
useEffect(() => {
setCategory(showDetailsData.category);
}, [showDetailsData.category]);
const handleNext = () => {
if (activeTab === "showDetails") {
// if (!category) {
// toast.error("Please select a category first.");
// return;
// }
setActiveTab("showUpload");
}
};
const handlePrevious = () => {
if (activeTab === "showUpload") {
setActiveTab("showDetails");
}
};
const handleCancel = () => {
toast.success("Upload Cancelled.");
setShowDetailsData(FINAL_initialState);
setUploadDetailsData({});
};
const handleUpload = () => {
const payload = {
...showDetailsData,
...UploadDetailsData,
};
console.log("FINAL DATA TO BACKEND: β
π₯", payload);
toast.success("Show Uploaded Successfully! π");
};
return (
<div className="w-full h-full flex flex-col">
{/* β
Tabs */}
<div className="w-full flex justify-between items-center p-4 bg-gray-700 rounded-md">
<Tabs
value={activeTab}
onValueChange={(val) => setActiveTab(val)}
className="w-[400px]"
>
<TabsList>
<TabsTrigger value="showDetails">Show Details</TabsTrigger>
<TabsTrigger value="showUpload">Show Upload</TabsTrigger>
</TabsList>
</Tabs>
</div>
<Separator className="my-2 bg-gray-600" />
<div className="p-4 bg-gray-700 rounded-md">
<ScrollArea className="w-full flex-1 overflow-auto h-[370px]">
{activeTab === "showDetails" && (
<ShowBasicDetails
showDetails={showDetailsData}
setShowDetails={setShowDetailsData}
/>
)}
{activeTab === "showUpload" && (
<ShowFileUpload
UploadDetails={UploadDetailsData}
setUploadDetails={setUploadDetailsData}
category={category}
/>
)}
</ScrollArea>
</div>
<div className="flex justify-between items-center mt-4">
{activeTab === "showDetails" && (
<>
<Button variant="outline" onClick={handleCancel}>
Cancel
</Button>
<Button onClick={handleNext} disabled={!category}>
Next
</Button>
</>
)}
{activeTab === "showUpload" && (
<>
<Button variant="outline" onClick={handlePrevious}>
Previous
</Button>
<Button onClick={handleUpload}>Upload</Button>
</>
)}
</div>
</div>
);
};
ShowBasicDetails.jsx
/* eslint-disable react/prop-types */
import { FINAL_showBasicFormControls } from "../../../../config/formFields";
import { AdminForm } from "../../../common/common-Form/adminForm";
export const ShowBasicDetails = ({ showDetails, setShowDetails }) => {
return (
<div className="flex-1 p-4 bg-gray-700 rounded-md">
<h2 className="text-lg font-semibold text-white mb-4">Details</h2>
<AdminForm
formControls={FINAL_showBasicFormControls}
formData={showDetails}
setFormData={setShowDetails}
/>
</div>
);
};
ShowFileUpload.jsx
import { useEffect, useState } from "react";
import { Button } from "../../../ui/button";
import { Label } from "../../../ui/label";
import { Input } from "../../../ui/input";
import { X } from "lucide-react";
import { toast } from "react-hot-toast";
import { AdminFileUpload } from "../../../common/common-Form/adminFileUpload";
export const ShowFileUpload = ({
category,
UploadDetails,
setUploadDetails,
}) => {
const [episodes, setEpisodes] = useState([
{ title: "", video: null, subtitle: null },
]);
const [movie, setMovie] = useState({
video: null,
subtitle: null,
});
// β
Reset Data When Category Changes π₯
useEffect(() => {
if (category === "movie") {
setMovie({ video: null, subtitle: null });
setUploadDetails({ movie: { video: null, subtitle: null } });
} else {
setEpisodes([{ title: "", video: null, subtitle: null }]);
setUploadDetails({
episodes: [{ title: "", video: null, subtitle: null }],
});
}
}, [category]);
// β
Sync Movie Data With Parent π₯
useEffect(() => {
if (category === "movie") {
setUploadDetails({ movie });
}
}, [movie]);
// β
Sync Episode Data With Parent π₯
useEffect(() => {
if (category === "webseries") {
setUploadDetails({ episodes });
}
}, [episodes]);
// β
Handle Add Episode π₯
const handleAddEpisode = () => {
setEpisodes([...episodes, { title: "", video: null, subtitle: null }]);
};
// β
Handle Remove Episode π₯
const handleRemoveEpisode = (index) => {
if (episodes.length === 1) {
toast.error("At least one episode is mandatory.");
return;
}
const updatedEpisodes = episodes.filter((_, i) => i !== index);
setEpisodes(updatedEpisodes);
};
// β
Handle File & Text Change π₯
const handleChange = (index, field, value) => {
const updatedEpisodes = [...episodes];
updatedEpisodes[index][field] = value;
setEpisodes(updatedEpisodes);
};
// β
Handle Video Removal π₯
const handleRemoveVideo = (index) => {
const updatedEpisodes = [...episodes];
updatedEpisodes[index].video = null;
setEpisodes(updatedEpisodes);
};
if (!category) {
return "Choose Category";
}
return (
<div className="flex-1 p-4 bg-gray-700 rounded-md">
<h2 className="text-lg font-semibold text-white mb-4">
{category === "movie" ? "Upload Movie & Subtitle" : "Upload Episodes"}
</h2>
{/* β
IF CATEGORY IS MOVIE */}
{category === "movie" ? (
<>
<Label className="text-white">Upload Movie</Label>
<AdminFileUpload
accept="video/*"
onUpload={(file) => setMovie((prev) => ({ ...prev, video: file }))}
/>
<Label className="text-white mt-2">Upload Subtitle</Label>
<AdminFileUpload
accept=".srt,.vtt"
onUpload={(file) =>
setMovie((prev) => ({ ...prev, subtitle: file }))
}
/>
</>
) : (
<>
<div className="flex justify-end mb-4">
<Button onClick={handleAddEpisode}>+ Add Episode</Button>
</div>
{/* β
LOOP THROUGH EPISODES */}
{episodes.map((episode, index) => (
<div
key={index}
className="p-4 border rounded-md bg-gray-800 mb-4 relative"
>
{/* β
REMOVE BUTTON AT TOP RIGHT */}
<div className="w-full flex justify-end mb-2.5">
{index > 0 && (
<Button
className="flex flex-row-reverse bg-red-900"
onClick={() => handleRemoveEpisode(index)}
>
Remove
</Button>
)}
</div>
{/* β
EPISODE TITLE */}
<Label className="text-white mt-2">Episode {index + 1}</Label>
<Input
value={episode.title}
onChange={(e) => handleChange(index, "title", e.target.value)}
placeholder={`Episode ${index + 1} Title`}
/>
{/* β
VIDEO UPLOAD */}
<Label className="text-white mt-2">Upload Video</Label>
<AdminFileUpload
accept="video/*"
onUpload={(file) => handleChange(index, "video", file)}
/>
{/* β
VIDEO PREVIEW */}
{episode.video && episode.video instanceof File && (
<div className="border-2 border-dashed rounded-lg flex items-center justify-center w-full h-[120px] mt-2 relative bg-gray-900">
<Button
size="icon"
variant="ghost"
className="absolute top-1 right-1 bg-red-800"
onClick={() => handleRemoveVideo(index)}
>
<X size={14} />
</Button>
</div>
)}
{/* β
SUBTITLE UPLOAD */}
<Label className="text-white mt-2">Upload Subtitle</Label>
<AdminFileUpload
accept=".srt,.vtt"
onUpload={(file) => handleChange(index, "subtitle", file)}
/>
</div>
))}
</>
)}
</div>
);
};
AdminFileUpload.jsx
/* eslint-disable react/prop-types */
import { FileIcon, UploadCloudIcon, XIcon } from "lucide-react";
import { Input } from "../../ui/input";
import { useRef, useState } from "react";
import { toast } from "react-hot-toast";
export const AdminFileUpload = ({
onUpload = () => {}, // β
It will directly pass fileUrl to FormData
accept = "image/*,video/*,.srt,.vtt",
text = "Upload File",
}) => {
const [file, setFile] = useState(null);
const inputRef = useRef(null);
// β
Handle File Upload Click
function handleClick() {
if (inputRef.current) inputRef.current.click();
}
// β
Handle File Selection
function handleFileChange(event) {
const selectedFile = event.target.files[0];
if (!selectedFile) {
toast.error("β File Upload Failed!");
return;
}
// β
Reset Input Lock
event.target.value = "";
// β
Generate File URL
const fileUrl = URL.createObjectURL(selectedFile);
// β
Save File State
setFile({
name: selectedFile.name,
url: fileUrl,
type: selectedFile.type,
size: selectedFile.size,
raw: selectedFile,
});
// β
Pass File URL Directly to FormData
onUpload(fileUrl);
// β
Show Confirmation Based on File Type
if (selectedFile.type.startsWith("video/")) {
toast.success("β
Video File Selected!");
} else if (
selectedFile.name.endsWith(".srt") ||
selectedFile.name.endsWith(".vtt")
) {
toast.success("β
Subtitle File Selected!");
} else {
toast.success(`β
${selectedFile.name} Uploaded Successfully!`);
}
}
// β
Handle File Removal
function handleRemoveFile(event) {
event.stopPropagation();
setFile(null);
onUpload(""); // β
Pass Empty URL to FormData
toast.success("File Removed Successfully!");
}
return (
<div className="w-full">
{/* β
UPLOAD BOX */}
<div
className="border-2 border-dashed rounded-lg p-4 cursor-pointer flex flex-col items-center justify-center relative"
onClick={handleClick}
>
<Input
type="file"
className="hidden"
ref={inputRef}
accept={accept}
onChange={handleFileChange}
/>
{/* β
FILE PREVIEW INSIDE BOX */}
{file ? (
<>
{/* β
REMOVE BUTTON INSIDE THE BOX */}
<div
className="absolute top-2 right-2 bg-red-500 rounded-full p-1 cursor-pointer"
onClick={handleRemoveFile}
>
<XIcon className="w-4 h-4 text-white" />
</div>
{/* β
IF IMAGE */}
{file.type.startsWith("image/") && (
<img
src={file.url}
alt="Preview"
className="w-full h-[150px] object-none rounded-lg "
/>
)}
{/* β
IF VIDEO */}
{file.type.startsWith("video/") && (
<video
src={file.url}
className="w-full h-[150px] object-none rounded-lg"
controls
/>
)}
{/* β
IF SUBTITLE */}
{(file.name.endsWith(".srt") || file.name.endsWith(".vtt")) && (
<div className="text-center text-white">
<span className="flex justify-center align-middle text-center gap-2">
<FileIcon className="w-6 h-6 text-primary" />
<p className=" font-medium ">{file.name}</p>
</span>
</div>
)}
</>
) : (
<>
{/* β
DEFAULT UPLOAD AREA */}
<UploadCloudIcon className="w-10 h-10 text-muted-foreground mb-2" />
<span className="text-gray-400">Click to select {text}</span>
</>
)}
</div>
</div>
);
};
AdminForm.jsx
/* eslint-disable no-undef */
/* eslint-disable react-refresh/only-export-components */
/* eslint-disable react/prop-types */
import { Input } from "../../ui/input";
import { Label } from "../../ui/label";
import { Button } from "../../ui/button";
import { Textarea } from "../../ui/textarea";
import { ToggleGroup, ToggleGroupItem } from "../../ui/toggle-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "../../ui/select";
import { Popover, PopoverContent, PopoverTrigger } from "../../ui/popover";
import { Calendar } from "../../ui/calendar";
import { AdminFileUpload } from "./adminFileUpload";
export const AdminForm = ({ formControls = [], formData, setFormData }) => {
// Function to render inputs dynamically based on component type
function renderInputsByComponentType(getControlItem) {
let element = null;
const value = formData[getControlItem.name] || "";
switch (getControlItem.componentType) {
// Input Field
case "input":
element = (
<Input
className="w-full p-2 border rounded"
name={getControlItem.name}
placeholder={getControlItem.placeholder}
id={getControlItem.name}
type={getControlItem.type}
value={value}
onChange={(event) =>
setFormData({
...formData,
[getControlItem.name]: event.target.value,
})
}
/>
);
break;
// Select Dropdown
case "select":
element = (
<Select
onValueChange={(value) =>
setFormData({ ...formData, [getControlItem.name]: value })
}
value={value}
>
<SelectTrigger className="w-full p-2 border rounded">
<SelectValue placeholder={getControlItem.label} />
</SelectTrigger>
<SelectContent className="w-full border rounded bg-white">
{getControlItem.options &&
getControlItem.options.map((optionItem) => (
<SelectItem key={optionItem.value} value={optionItem.value}>
{optionItem.label}
</SelectItem>
))}
</SelectContent>
</Select>
);
break;
case "textarea":
element = (
<Textarea
className="w-full p-2 border rounded min-h-25"
name={getControlItem.name}
placeholder={getControlItem.placeholder}
id={getControlItem.name}
type={getControlItem.type}
value={value}
onChange={(event) =>
setFormData({
...formData,
[getControlItem.name]: event.target.value,
})
}
/>
);
break;
// Toggle Group (Genre Selection)
case "toggle-group":
element = (
<ToggleGroup
type="multiple"
className="flex flex-wrap gap-2"
value={formData[getControlItem.name] || []} // Ensure it's always an array
onValueChange={(value) =>
setFormData({
...formData,
[getControlItem.name]: Array.isArray(value) ? value : [value], // Ensure it's always an array
})
}
>
{getControlItem.options.map((optionItem) => (
<ToggleGroupItem
key={optionItem.value}
value={optionItem.value}
className={`px-3 py-2 border rounded ${
formData[getControlItem.name]?.includes(optionItem.value)
? "bg-gray-300" // Add active state styling
: ""
}`}
>
{optionItem.label}
</ToggleGroupItem>
))}
</ToggleGroup>
);
break;
// Date Picker (Better Date Format)
case "date":
element = (
<Popover>
<PopoverTrigger asChild>
<Button
variant="outline"
className={`w-full p-2 border rounded ${
!formData[getControlItem.name] && "text-muted-foreground"
}`}
>
{formData[getControlItem.name]
? new Date(formData[getControlItem.name]).toLocaleDateString()
: "Pick a date"}
</Button>
</PopoverTrigger>
<PopoverContent
className="w-auto p-0 z-[1000] pointer-events-auto"
align="start"
>
<Calendar
className="rounded-md border-none shadow bg-white"
mode="single"
selected={
formData[getControlItem.name]
? new Date(formData[getControlItem.name])
: undefined
}
onSelect={(selectedDate) => {
if (selectedDate) {
setFormData({
...formData,
[getControlItem.name]: selectedDate
.toISOString()
.split("T")[0], // Saves as YYYY-MM-DD
});
}
}}
initialFocus
/>
</PopoverContent>
</Popover>
);
break;
// File Upload (Dynamic Accept Type)
case "file":
element = (
<AdminFileUpload
accept={getControlItem.accept || "image/*,video/*,.srt,.vtt"}
text={getControlItem.label}
onUpload={(fileUrl) =>
setFormData({
...formData,
[getControlItem.name]: fileUrl,
})
}
/>
);
break;
default:
element = (
<Input
className="w-full p-2 border rounded"
name={getControlItem.name}
placeholder={getControlItem.placeholder}
id={getControlItem.name}
type={getControlItem.type}
value={value}
onChange={(event) =>
setFormData({
...formData,
[getControlItem.name]: event.target.value,
})
}
/>
);
break;
}
return element;
}
return (
<div className="flex flex-col gap-3 ">
{formControls.map((controleItem) => (
<div key={controleItem.name}>
<Label htmlFor={controleItem.name}>{controleItem.label}</Label>
{renderInputsByComponentType(controleItem)}
</div>
))}
</div>
);
};
The crucial point is that you didn't write back the form value with the file type:
// adminForm.jsx
// ...
export const AdminForm = ({ formControls = [], formData, setFormData }) => {
// Function to render inputs dynamically based on component type
function renderInputsByComponentType(getControlItem) {
let element = null;
const value = formData[getControlItem.name] || "";
switch (getControlItem.componentType) {
// ...
// Write back form value
case "file":
element = (
<AdminFileUpload
accept={getControlItem.accept || "image/*,video/*,.srt,.vtt"}
text={getControlItem.label}
onUpload={(fileUrl) =>
setFormData({
...formData,
[getControlItem.name]: fileUrl,
})
}
/>
);
break;
// ...
}
return element;
}
return (
// ...
);
};
You need to pass the file in the AdminForm
and receive it in AdminFileUpload
to echo a preview of the file:
// adminFileUpload.jsx
// ...
export const AdminFileUpload = ({
onUpload = () => {},
accept = "image/*,video/*,.srt,.vtt",
text = "Upload File",
file // Receive the file
}) => {
// ...
return (
// ...
{/* β
IF IMAGE */}
{file.type.startsWith("image/") && (
<img
src={file.url}
alt="Preview"
className="w-full h-[150px] object-none rounded-lg "
/>
)}
{/* β
IF VIDEO */}
{file.type.startsWith("video/") && (
<video
src={file.url}
className="w-full h-[150px] object-none rounded-lg"
controls
/>
)}
{/* β
IF SUBTITLE */}
{(file.name.endsWith(".srt") || file.name.endsWith(".vtt")) && (
<div className="text-center text-white">
<span className="flex justify-center align-middle text-center gap-2">
<FileIcon className="w-6 h-6 text-primary" />
<p className=" font-medium ">{file.name}</p>
</span>
</div>
)}
// ...
)
}