How to immediately update data using TanStack in NextJS 13

107 Views Asked by At

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 };
    },
0

There are 0 best solutions below