#Schedule an invalidation of a query

4 messages · Page 1 of 1 (latest)

dusty arch
#

I have a page that displays either videos or images, one at a time. Videos play until they finish, while images are displayed for 5, 8, or 15 seconds. Once a video or image ends, the next one is shown. I fetch these videos and images through a query, which is invalidated upon a real-time database change to update the content. However, I don't want the query to be invalidated immediately when a change occurs. Instead, I want the query to wait until the current video finishes or the image's set duration has elapsed. Only then should it invalidate, ensuring a smooth user experience.

Code:

#
"use client";

import React, { useState, useEffect, useRef, useCallback } from "react";
import { useGetAdvertisements } from "~/src/hooks/useGetAdvertisements";
import { FullScreen, useFullScreenHandle } from "react-full-screen";
import Image from "next/image";
import { Maximize2 } from "lucide-react";
import { Button } from "@repo/ui/components/ui/button";

const Advertise = ({ params }: { params: { companyName: string } }) => {
  const { data, isLoading, error } = useGetAdvertisements(params.companyName);
  const [currentAdIndex, setCurrentAdIndex] = useState(0);
  const handle = useFullScreenHandle();
  const videoRef = useRef<HTMLVideoElement>(null);
  const timerRef = useRef<NodeJS.Timeout | null>(null);

  const rotateAd = useCallback(() => {
    if (data?.ads) {
      setCurrentAdIndex((prevIndex) => (prevIndex + 1) % data.ads.length);
    }
  }, [data]);

  useEffect(() => {
    if (data?.ads && data.ads.length > 0) {
      const currentAd = data.ads[currentAdIndex];

      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }

      if (currentAd.videoUrl) {
        const videoElement = videoRef.current;
        if (videoElement) {
          videoElement.src = currentAd.videoUrl;
          videoElement.load();
          videoElement.play().catch(console.error);

          const handleVideoEnd = () => {
            rotateAd();
          };

          videoElement.addEventListener("ended", handleVideoEnd);
          return () =>
            videoElement.removeEventListener("ended", handleVideoEnd);
        }
      } else {
        timerRef.current = setTimeout(rotateAd, currentAd.displayDuration);
      }
    }

    return () => {
      if (timerRef.current) {
        clearTimeout(timerRef.current);
      }
    };
  }, [data, currentAdIndex, rotateAd]);

Continued...

#
  useEffect(() => {
    const handleKeyPress = (event: KeyboardEvent) => {
      if (event.key === "f" || event.key === "F") {
        handle.active ? handle.exit() : handle.enter();
      }
    };

    document.addEventListener("keydown", handleKeyPress);

    return () => {
      document.removeEventListener("keydown", handleKeyPress);
    };
  }, [handle]);

  if (isLoading) return <div>Indlæser...</div>;
  if (error) return <div>Fejl: {error.message}</div>;
  if (!data || !data.ads || data.ads.length === 0)
    return <div>Ingen annoncer tilgængelige</div>;

  console.log("Nuværende annonceindeks:", currentAdIndex);
  console.log(data);

  const currentAd = data.ads[currentAdIndex];

  return (
    <FullScreen handle={handle}>
      <div className="relative w-full h-screen bg-black overflow-hidden">
        {currentAd && currentAd.videoUrl ? (
          <video
            ref={videoRef}
            className="w-full h-full object-fit"
            autoPlay
            muted
          />
        ) : (
          <img
            className="w-full h-full object-cover"
            src={currentAd.imageUrl!}
            alt={currentAd.title}
          />
        )}
        {!handle.active && (
          <Button
            onClick={handle.enter}
            className="absolute top-4 right-4 opacity-90"
            size="icon"
          >
            <Maximize2 className="h-4 w-4 text-white" />
            <span className="sr-only">Skift til fuld skærm</span>
          </Button>
        )}
      </div>
    </FullScreen>
  );
};

export default Advertise;
#

and this is where I fetch/invalidate:

"use client";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect } from "react";
import { createBrowserClient } from "@supabase/ssr";
import { getAdvertisements } from "../actions/getAdvertisements";

export const useGetAdvertisements = (companyName: string) => {
  const queryClient = useQueryClient();

  const { data, isLoading, error } = useQuery({
    queryKey: ["company", companyName],
    queryFn: () => getAdvertisements(companyName),
  });

  useEffect(() => {
    const supabase = createBrowserClient(
      process.env.NEXT_PUBLIC_SUPABASE_URL!,
      process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    );

    const channel = supabase
      .channel("db-changes")
      .on(
        "postgres_changes",
        {
          event: "*",
          schema: "public",
          table: "Advertisement",
        },
        (payload) => {
          console.log("Change received!", payload);
          queryClient.invalidateQueries({ queryKey: ["company", companyName] });
        },
      )
      .subscribe((status) => {
        console.log("Subscription status:", status);
      });

    return () => {
      supabase.removeChannel(channel);
    };
  }, [companyName, queryClient]);

  return { data, isLoading, error };
};