Я использовал useActionState вместе с InitialState.
ПроблемаЕсли после отправки в серверной части произошла ошибка проверки, данные, за исключением файлов, возвращались во внешний интерфейс. поэтому пользователю приходится повторно выбирать их все.
Чтобы избежать такого поведения, я сохранил файлы в определенном состоянии (больше не во входных данных), это был лучший подход, поскольку я не могу переназначить данные в поле ввода типа файла.
Однако я не знаю, как прикрепить их к запросу, идущему к serverAction.
Что я пробовал
Я пытались сделать это следующим образом:
Код: Выделить всё
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
files.forEach((file) => formData.append("proof", file));
formAction(formData);
};
Код: Выделить всё
An async function was passed to useActionState, but it was dispatched outside of an action context. This is likely not what you intended. Either pass the dispatch function to an `action` prop, or dispatch manually inside `startTransition`
Цель/Ожидание
- Как упоминалось выше, я хочу хранить файлы до тех пор, пока они не будут успешно загружены в облако.
- В случае успеха я хочу сбросить состояния и удалить файлы из состояния.
Код: Выделить всё
"use client";
import { useActionState, useCallback, useEffect } from "react";
import submit from "./action";
import { twJoin } from "tailwind-merge";
import Main from "@/components/Main";
import { useLocale, useTranslations } from "next-intl";
import Dropzone from "./Dropzone";
import PageHeader from "@/components/PageHeader";
import HugeRoundedButton from "@/components/RoundedButton";
import { useState } from "react";
import { FileRejection } from "react-dropzone";
const initialState = {
zodErrors: null,
message: null,
success: false,
data: {
summary: "",
proof: [],
note: "",
},
};
interface PreviewFile extends File {
preview: string;
}
const CostTracker = () => {
// States
const [formState, formAction, pending] = useActionState(submit, initialState);
const [files, setFiles] = useState([]);
const [rejectedFiles, setRejectedFiles] = useState([]);
const onDrop = useCallback(
(acceptedFiles: File[], rejectedFiles: FileRejection[]) => {
if (acceptedFiles.length > 0) {
setFiles((previousFiles) => [
...previousFiles,
...acceptedFiles.map((file) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
),
]);
}
if (rejectedFiles.length > 0) {
setRejectedFiles((previousFiles) => [
...previousFiles,
...rejectedFiles,
]);
}
},
[]
);
const removeFile = (name: string) => {
// Revoke the data uris to avoid memory leaks
URL.revokeObjectURL(
files.find((file) => file.name === name)?.preview as string
);
setFiles((files) => files.filter((file) => file.name !== name));
};
const removeRejectedFile = (name: string) => {
setRejectedFiles((rejectedFiles) =>
rejectedFiles.filter(({ file }) => file.name !== name)
);
};
// Runs when the component unmounts
useEffect(() => {
// Make sure to revoke the data uris to avoid memory leaks, will run on unmount
return () => files.forEach((file) => URL.revokeObjectURL(file.preview));
}, [files]);
// Locale
const locale = useLocale();
const translations = useTranslations("cost_tracking_page");
const dir = locale === "ar" ? "rtl" : "ltr";
// Form State
const { data, zodErrors, message, success } = formState;
// Handle form submission
const handleFormSubmit = (e: React.FormEvent) => {
e.preventDefault();
const formData = new FormData(e.target as HTMLFormElement);
files.forEach((file) => formData.append("proof", file));
formAction(formData);
};
return (
{zodErrors?.summary && (
{zodErrors.summary}
)}
{zodErrors?.note && (
{zodErrors.note}
)}
aria-live="assertive"
className={twJoin(success ? "text-green-500" : "text-red-500")}
>
{message}
);
};
export default CostTracker;
Спасибо всем.
Подробнее здесь: https://stackoverflow.com/questions/793 ... er-actions
Мобильная версия