javascriptreactjs

File upload preview disappears when switching tabs (State persists)


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:

  1. βœ… I want the file preview to persist even after switching tabs.
  2. βœ…The uploaded file URL should stay intact, and the preview should load from the URL without re-uploading the file.
  3. βœ…

πŸ›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>
  );
};


Solution

  • 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>
        )}
    
        // ...
      )
    }