I have a problem with updating images when the user deletes them or uploads new ones. I use TanStack Query for it. There is my ImagesPage.tsx file:
"use client";
// imports
function ImagesPage() {
const { data: session, status } = useSession();
const queryClient = useQueryClient();
const toastId = "fetched-nationalities";
const [isDeleteCategory, setIsDeleteCategory] = useState<boolean>(false);
const [urlToDelete, setUrlToDelete] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState<boolean>(false);
const [selectedCategory, setSelectedCategory] = useState<Key | null>(null);
const [isImageLoaded, setImageLoaded] = useState<boolean>(false);
const [isDropdownOpen, setIsDropdownOpen] = useState<boolean>(false);
const [deleteCategoryName, setDeleteCategoryName] = useState<string | null>(
null,
);
useEffect(() => {
if (selectedCategory) {
handleImagesByCategory({ category: selectedCategory, session });
}
}, [selectedCategory]);
useEffect(() => {
if (isDeleteCategory) {
setIsDropdownOpen(true);
setSelectedCategory("all");
} else {
setIsDropdownOpen(false);
}
}, [isDeleteCategory]);
const {
data: allImages,
isFetching: isFetchingAllImages,
isLoading: isLoadingAllImages,
} = useQuery<string[]>({
queryKey: ["images"],
queryFn: async () => await handleAllImages({ session }),
enabled: !!session?.user.accessToken,
refetchOnMount: "always",
refetchInterval: false,
});
const {
data: imagesByCategory,
isFetching: isFetchingImagesByCategory,
isLoading: isLoadingImagesByCategory,
} = useQuery<string[]>({
queryKey: ["images", selectedCategory],
queryFn: () =>
handleImagesByCategory({ category: selectedCategory, session }),
refetchOnMount: "always",
enabled:
!!selectedCategory &&
selectedCategory !== "all" &&
!!session?.user.accessToken,
});
const imagesToDisplay =
selectedCategory && selectedCategory !== "all"
? imagesByCategory
: allImages;
const isFetching =
selectedCategory && selectedCategory !== "all"
? isFetchingImagesByCategory
: isFetchingAllImages;
const { mutate: deleteImageMutation, isPending } = useMutation({
mutationFn: async (url: string | null | undefined) => {
await handleDeleteImage({
url,
session,
setIsModalOpen,
});
},
onMutate: async (url: string | null | undefined) => {
await queryClient.cancelQueries({
queryKey: ["images"],
});
const previousImages = queryClient.getQueryData(["images"]);
queryClient.setQueryData(
["images"],
(old: string[] | undefined) =>
old?.filter((image: string) => image !== url),
);
return { previousImages };
},
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: ["images"],
});
if (selectedCategory) {
await queryClient.invalidateQueries({
queryKey: ["images", selectedCategory],
});
}
},
onError: (error) => {
console.error("Error deleting the image:", error);
toast.error(`${error}`, { toastId });
},
});
if (
status === "loading" ||
isPending ||
isFetching ||
isLoadingAllImages ||
isLoadingImagesByCategory
) {
return <Loader />;
}
return (
<div className="flex pb-12 pt-4 items-center justify-center flex-col">
<div className="flex gap-4 mb-10 justify-center items-center flex-wrap">
<CategoryTabs
selectedCategory={selectedCategory}
setSelectedCategory={setSelectedCategory}
isDeleteCategory={isDeleteCategory}
setIsDeleteCategory={setIsDeleteCategory}
setDeleteCategoryName={setDeleteCategoryName}
isDropdownOpen={isDropdownOpen}
deleteCategoryName={deleteCategoryName}
/>
<Button
isIconOnly
size="sm"
radius="full"
color="primary"
variant="flat"
className="bg-gray-300 p-0"
onClick={() => {
setIsDropdownOpen(() => !isDropdownOpen);
}}
>
{isDropdownOpen ? (
<BsChevronUp className="h-4 w-4" />
) : (
<BsChevronDown className="h-4 w-4" />
)}
</Button>
</div>
{imagesToDisplay?.length === 0 && !isFetching && (
<div
id="alert"
className="flex items-center p-4 mb-4 text-yellow-800 rounded-lg bg-slate-300 dark:bg-gray-800 dark:text-yellow-300"
role="alert"
>
<FaCircleInfo />
<div className="ml-3 text-base font-medium">
No images in the selected category. Please{" "}
<Link
href="/dashboard"
className="font-semibold text-yellow-950 underline hover:no-underline"
>
upload
</Link>{" "}
some, and they`ll appear here.
</div>
</div>
)}
{imagesToDisplay?.length !== 0 && (
<div className="grid sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-8">
{imagesToDisplay?.map((url) => {
const handleImageLoad = () => {
setImageLoaded(true);
};
const handleDeleteClick = () => {
setIsModalOpen(true);
setUrlToDelete(url);
};
return (
<div key={url} className="w-50 relative group">
{!isImageLoaded && <SkeletonImage />}
<Image
className={`ring-offset-4 ring-4 ring-gray-500 ring-offset-gray-400 duration-300 rounded-lg hover:ring-slate-800 ${
isImageLoaded ? "" : "hidden"
}`}
src={url}
width={200}
height={200}
alt={`${url}`}
priority={true}
onLoad={handleImageLoad}
style={{
width: "200px",
height: "200px",
}}
/>
<button
className="absolute bottom-0 flex items-center justify-center w-full bg-black rounded-b-lg bg-opacity-60 text-white opacity-0 group-hover:opacity-100 transition-opacity duration-300 h-12"
onClick={handleDeleteClick}
>
Delete
</button>
</div>
);
})}
</div>
)}
{isModalOpen && (
<DeleteConfirmationModal
buttonText={"Delete Image"}
isOpen={isModalOpen}
url={urlToDelete}
title="Confirm Deletion"
description="Are you sure you want to delete this image? This action cannot be undone."
onClose={() => setIsModalOpen(false)}
onOpenChange={deleteImageMutation}
/>
)}
</div>
);
}
export default ImagesPage;
Method for retrieving all images:
import { toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import { Session } from "next-auth";
const handleAllImages = async ({ session }: { session: Session | null }) => {
const toastId = "fetched-nationalities";
try {
const response = await fetch(`${process.env.BACKEND_URL}/images`, {
method: "GET",
headers: {
authorization: `Bearer ${session?.user.accessToken}`,
},
cache: "no-store",
next: { revalidate: 0 },
});
const data = await response.json();
console.log(data.length);
if (response.ok) {
return data;
} else {
toast.error(`${data.message}`, { toastId });
}
} catch (error) {
toast.error(`${error}`, { toastId });
console.error(error);
}
};
export default handleAllImages;
Method for uploading image:
const { data: session, status } = useSession();
const queryClient = useQueryClient();
const toastId = "fetched-nationalities";
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [imageTitle, setImageTitle] = useState<string>("");
const [selectedCategory, setSelectedCategory] = useState<string>("");
const { mutate: uploadImageMutation, isPending } = useMutation({
mutationFn: async (formData: FormData) => {
const response = await fetch(`${process.env.BACKEND_URL}/upload`, {
method: "POST",
body: formData,
headers: {
authorization: `Bearer ${session?.user.accessToken}`,
},
});
const data = await response.json();
if (!response.ok) {
toast.error(`${data.message}`, { toastId });
}
},
onMutate: async (newImage) => {
await queryClient.cancelQueries({ queryKey: ["images"] });
const previousImages = queryClient.getQueryData(["images"]);
queryClient.setQueryData(["images"], (old: string[]) => [
...old,
newImage,
]);
return { previousImages };
},
onSettled: async (error, context: any) => {
if (error) {
await queryClient.setQueryData(["images"], context.previousImages);
console.error(error);
toast.error(`${error}`, { toastId });
}
await queryClient.invalidateQueries({ queryKey: ["images"] });
toast.success("Image uploaded successfully", { toastId });
setSelectedFile(null);
setImageTitle("");
setSelectedCategory("");
},
});
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (selectedFile && selectedCategory) {
const formData = new FormData();
formData.append("file", selectedFile);
formData.append("title", imageTitle);
formData.append("category", selectedCategory);
uploadImageMutation(formData);
}
};
Images are sent to string[] from the backend
I tried to use it in Mutation (but it doesn't work):
onMutate: async (newImage) => {
await queryClient.cancelQueries({ queryKey: ["images"] });
const previousImages = queryClient.getQueryData(["images"]);
queryClient.setQueryData(["images"], (old: string[]) => [
...old,
newImage,
]);
return { previousImages };
},