T
TanStack11mo ago
optimistic-gold

Schedule an invalidation of a query

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:
1 Reply
optimistic-gold
optimistic-goldOP11mo ago
"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]);
"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;
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 };
};
"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 };
};
My question is; how can I achieve to schedule an invalidation based on my realtime database change event?

Did you find this page helpful?