Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issues with Handling File Inputs and Image Previews in Next.js 15 Form Actions #72958

Open
Marabii opened this issue Nov 19, 2024 · 8 comments
Open
Labels
bug Issue was opened via the bug report template.

Comments

@Marabii
Copy link

Marabii commented Nov 19, 2024

Link to the code that reproduces this issue

https://github.com/Marabii/next-js-issue

To Reproduce

Minimal Reproducible Example:

Client Component (Home.tsx):

"use client";

import { useActionState } from "react";
import { useState } from "react";
import sendDataAction, { State } from "./action";
import Image from "next/image";

export default function Home() {
  const [state, formAction, pending] = useActionState<State, FormData>(
    sendDataAction,
    null
  );
  const [selectedImages, setSelectedImages] = useState<File[]>([]);

  const handleImageChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    if (event.target.files) {
      setSelectedImages(Array.from(event.target.files));
    }
  };

  const handleImageRemove = (index: number) => {
    setSelectedImages((prevImages) =>
      prevImages.filter((_, imgIndex) => imgIndex !== index)
    );
  };

  return (
    <div className="flex items-center justify-center min-h-screen bg-gray-100">
      <form
        action={formAction}
        className="bg-white shadow-md rounded-lg p-8 space-y-6 w-full max-w-md"
      >
        <div>
          <label
            htmlFor="images"
            className="block text-sm font-medium text-gray-700 mb-2"
          >
            Upload Images
          </label>
          <input
            type="file"
            name="images"
            id="images"
            multiple
            onChange={handleImageChange}
            required
            className="block w-full text-sm text-gray-900 border border-gray-300 rounded-lg cursor-pointer bg-gray-50 focus:outline-none focus:ring focus:ring-indigo-500 focus:border-indigo-500"
          />
          <div className="mt-4">
            <h2 className="text-sm font-medium text-gray-800 mb-2">
              Selected Images
            </h2>
            <div className="grid grid-cols-2 gap-2">
              {selectedImages.map((image, index) => (
                <div
                  key={index}
                  className="relative w-full h-24 bg-gray-100 rounded overflow-hidden border border-gray-200"
                >
                  <Image
                    src={URL.createObjectURL(image)}
                    alt={`Selected image ${index + 1}`}
                    layout="fill"
                    objectFit="cover"
                    className="rounded"
                  />
                  <button
                    type="button"
                    onClick={() => handleImageRemove(index)}
                    className="absolute top-1 right-1 bg-red-500 text-white rounded-full p-1 hover:bg-red-600 focus:outline-none"
                  ></button>
                </div>
              ))}
            </div>
          </div>
        </div>
        <button
          disabled={pending}
          type="submit"
          className={`w-full py-2 px-4 rounded-lg text-white ${
            pending
              ? "bg-gray-400 cursor-not-allowed"
              : "bg-indigo-600 hover:bg-indigo-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
          }`}
        >
          {pending ? "Submitting..." : "Submit"}
        </button>
        <div className="text-sm text-gray-500 mt-4">
          <pre>{JSON.stringify(state, null, 2)}</pre>
        </div>
      </form>
    </div>
  );
}

Server Action (action.ts):

"use server";

const sendDataAction = async (
  state: State,
  formData: FormData
): Promise<State> => {
  // Simulating a validation error
  return {
    status: "failure",
    errors:
      "This isn't a real error, but just simulating an error that could occur in a big form with multiple inputs",
  };
};

export default sendDataAction;

export type State = {
  status: "success" | "failure";
  errors?: string;
} | null;

Steps to Reproduce:

  1. Create a Next.js 15 application with the above client and server components.
  2. Run the application and navigate to the form.
  3. Select multiple image files using the file input.
  4. Click the "Submit" button to trigger the form submission.
  5. Observe that:
    • The file input resets, requiring re-selection of images.
    • The image previews remain displayed due to the selectedImages state.
    • If attempting to remove an image from the preview, the file input does not reflect this change.

Current vs. Expected behavior

Description:
I'm experiencing challenges when working with file inputs, specifically image uploads, in a Next.js 15 application using form actions. The issues arise when attempting to preview selected images and implement a remove functionality. Below are the detailed problems and a minimal reproducible example.

Issues Encountered:

  1. Image Inputs Reset on Submit with Validation Errors:

    • When the form is submitted, the file input resets, causing the user to reselect images if there are validation errors handled in the server action.
    • Despite the file input resetting, the selectedImages state on the client side still holds the previously selected images and displays the previews.
    • This discrepancy leads to confusion as the user sees the previews but is required to re-upload the images.
  2. Inability to Synchronize File Input with Selected Images State:

    • It's not possible to programmatically set the value of the file input (e.g., value={selectedImages}) due to security restrictions.
    • This limitation prevents synchronizing the file input with the selectedImages state, especially when users remove images from the preview. The file input remains unchanged, leading to inconsistencies.

Impact:
These issues hinder the user experience by creating confusion between the displayed image previews and the actual state of the file input. Users may find it frustrating to re-upload images after encountering validation errors, and the inability to manage the file input state programmatically limits the functionality of the form.

Expected Behavior:

  • On Validation Errors:

    • The file input should retain the selected files to prevent users from re-uploading them.
    • The image previews should accurately reflect the current state of the file input.
  • Synchronizing File Input with State:

    • It should be possible to programmatically manage the file input's value to match the selectedImages state. I know it's not currently possible due to security reasons, but there has to be a workaround.

Provide environment information

Operating System:
 Platform: linux mint 22 cinamon
Relevant packages:
 next: 15.0.3
 react and react-dom: 19.0.0-rc-66855b96-20241106

Which area(s) are affected? (Select all that apply)

Not sure

Which stage(s) are affected? (Select all that apply)

next dev (local), next build (local), next start (local), Vercel (Deployed), Other (Deployed)

Additional context

No response

@Marabii Marabii added the bug Issue was opened via the bug report template. label Nov 19, 2024
@master4o
Copy link

master4o commented Nov 20, 2024

model of server actions is broken, its reset form everytime even when form action return errors (in docs Next recommend to return errors in objects - https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations#server-side-form-validation).

As for me, i decide to use formData cycle for error state. In server action Next passes formData, we can return object with formData from server action and can pass values in inputs after. Next resets values, we returns them back :)

P.S. You can use this in all cases (not only for error state) in your example for some kind of inputs

@Marabii
Copy link
Author

Marabii commented Nov 20, 2024

Thank you for your response! Your idea is excellent—I can’t believe I didn’t think of it myself. Unfortunately, it doesn’t resolve the issue I’m facing. While I do have access to the formData object after validation, I’m unable to programmatically push file data into the input due to security restrictions. This limitation is particularly challenging for file inputs. For other input types, I’ve worked around it by creating a custom input component that stores the input data in state and programmatically updates it.

@master4o
Copy link

master4o commented Nov 20, 2024

@Marabii, try to add state.formData.get('images') in defaultValue prop and as initValue in useState

@Marabii
Copy link
Author

Marabii commented Nov 20, 2024

Thanks for trying to help me but it doesn't work. I really think there is no way to do this unless they developed a specific Input file component for ths usecase.

@liri2006
Copy link

@Marabii in order to preserve file you can use useState hook and then manually append it to formData on form submit:

export default function AccountForm() {
  const [selectedFile, setSelectedFile] = useState<File | null>(null)
  const [formState, formAction, isPending] = useActionState(saveMyAccount, null)

  const handleSubmit = (formData: FormData) => {
      if (selectedFile) formData.set('apiKey', selectedFile)
      formAction(formData)
  }

  const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0] || null
    setSelectedFile(file)
  }

  return (
    <form action={handleSubmit} className="mx-auto max-w-4xl">
       <input
          id="dropzone-file"
          name="apiKey"
          type="file"
          onChange={handleFileChange}
        />
     </form>
   )
}

@Marabii
Copy link
Author

Marabii commented Nov 21, 2024

Your idea works, thanks a lot but I hope there was a better way of doing it. I didn't want to mess with the formData object but i guess there is no other solution. I'm closing this issue, thanks a lot @liri2006 and @master4o

@Marabii Marabii closed this as completed Nov 21, 2024
@liri2006
Copy link

@Marabii yeah, I was also disappointed, it seems like useState-less form handling is what they are after here with serverActions, but it supports only basic use cases with plain inputs for now. Even select does not work without additional hassle.
Maybe keep issue open, so developers know about these shortcomings?

@Marabii Marabii reopened this Nov 21, 2024
@Marabii
Copy link
Author

Marabii commented Nov 21, 2024

I reopened this issue per @liri2006 request, it seems like manual manipulation of the formdata object is mandatory in cases like file inputs and select inputs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Issue was opened via the bug report template.
Projects
None yet
Development

No branches or pull requests

3 participants