I encouter some really weird behavior with my React component. I am completely new to React. I have one page on which the user can configure some toasts that will be displayed. I am making us of the react-hot-toast package. The user fills in a google place id whereafter toasts with reviews will be displayed. Everything is working fine until the following point:
When I clicked apply for the first time, the first toasts are showing up fine. However, when I then edit some variables like the position of the toast, the new toasts show up, but the old toasts (with the old position) are still showing up! It seems like when I click apply for the second time, two instances of the PopTrustToast component are alive, since console.log("position", position); is printing bottom-left and also top-right for example.
When I click apply, I want the old PopTrustToast component to be deleted and start with a fresh one. ChatGPT unfortunately could not help me with this. If anyone can, this would be extremely helpful for me!
Dashboard -> page.tsx
"use client";
import ButtonAccount from "@/components/ButtonAccount";
import config from "@/config";
import logo from "@/app/icon.png";
import Image from "next/image";
import { CodeBlock, CopyBlock, dracula } from 'react-code-blocks';
import React, { useEffect, useState } from 'react';
import Footer from "@/components/Footer";
import PopTrustToast from "@/components/PopTrustToast";
import { ToastPosition } from "react-hot-toast";
export const dynamic = "force-dynamic";
export default function Dashboard() {
useEffect(() => {
const handleClickOutside = () => {
document.querySelectorAll('.dropdown').forEach(function (dropdown) {
// Click was outside the dropdown, close it
(dropdown as HTMLDetailsElement).open = false;
});
};
// Attach the event listener to window
window.addEventListener('click', handleClickOutside);
// Cleanup the event listener on component unmount
return () => window.removeEventListener('click', handleClickOutside);
}, []);
const [placeId, setPlaceId] = useState('');
const [shouldFetch, setShouldFetch] = useState(false);
const [toastPosition, setToastPosition] = useState<ToastPosition>('bottom-right'); // default position
const [isOpen, setIsOpen] = useState(false); // State to control dropdown open status
const [initialWaitTime, setInitialWaitTime] = useState(2); // State for initial wait time
const [frequencyTime, setFrequencyTime] = useState(6); // State for frequency time
const [durationTime, setDurationTime] = useState(8); // State for duration time
const [minRating, setMinRating] = useState(5); // State for minimum rating
const [key, setKey] = useState(0); // State to force re-render
const handlePlaceIdChange = (event: { target: { value: React.SetStateAction<string>; }; }) => {
console.log(event.target.value);
setPlaceId(event.target.value);
};
const handleApplyClick = () => {
setShouldFetch(true);
setKey(prevKey => prevKey + 1); // Increment key to force re-render
};
const formatPositionText = (position: string): string => {
return position.split('-').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
};
return (
<>
<main className="p-8 mb-20">
<section className="max-w-3xl mx-auto space-y-8 ">
<nav className="container flex items-center justify-between px-0 py-4 mx-auto" aria-label="Global">
<div className="flex lg:flex-1 items-center">
<Image
src={logo}
alt={`${config.appName} logo`}
priority={true}
width={32}
height={32}
/>
<span className="font-extrabold text-lg">{config.appName}</span>
</div>
<div className="flex-grow"></div>
<ButtonAccount />
</nav>
<div className="flex flex-col items-center justify-center text-center">
<h2 className="font-bold text-6xl md:text-2xl tracking-tight mb-2">Reviews popup configuration</h2>
<p className="label-text mb-6 text-gray-400">Your popups will be displayed on this screen to showcase them</p>
<div className="flex flex-wrap justify-center gap-10">
{/* First column */}
<div>
<div className="form-control w-full max-w-xs mb-4">
<div className="label">
<span className="label-text">Google Maps Place ID</span>
<a href="https://developers.google.com/maps/documentation/places/web-service/place-id#:~:text=Are%20you%20looking%20for%20the%20place%20ID%20of%20a%20specific%20place%3F%20Use%20the%20place%20ID%20finder%20below%20to%20search%20for%20a%20place%20and%20get%20its%20ID%3A" target="_blank" rel="noopener noreferrer" className="underline text-gray-500 ml-2">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</a>
</div>
<input type="text" value={placeId} onChange={handlePlaceIdChange} placeholder="ChIJN0uYIOqwxUcR6qshtf23qiA" className="input input-bordered w-full max-w-xs" />
{/* <div className="mt-2 flex items-center text-sm">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth="2">
<path strokeLinecap="round" strokeLinejoin="round" d="M13 16h-1v-4h1m0-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<a href="https://www.vendasta.com/blog/gbp-url/#:~:text=are%20being%20shared.-,How%20to%20find%20the%20Google%20Business%20Profile%20URL,-By%20following%20these" target="_blank" rel="noopener noreferrer" className="underline text-gray-500 ml-2">
Where do I find this?
</a>
</div> */}
</div>
<div className="w-full max-w-xs text-left">
<div className="label">
<span className="label-text">Position</span>
</div>
</div>
<details className="dropdown w-full max-w-xs mb-4" style={{ zIndex: 2 }} open={isOpen} >
<summary className="btn w-full">{formatPositionText(toastPosition)}</summary>
<ul className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
{['top-left', 'top-right', 'bottom-left', 'bottom-right'].map(position => (
<li key={position} onClick={() => {
setToastPosition(position as ToastPosition);
}}>
<a>{formatPositionText(position)}</a>
</li>
))}
</ul>
</details>
<div className="w-full max-w-xs text-left">
<div className="label">
<span className="label-text">Theme (style)</span>
</div>
</div>
<details className="dropdown w-full max-w-xs mb-4 z" style={{ zIndex: 1 }}>
<summary className="btn w-full">Light</summary>
<ul className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
<li><a>Light</a></li>
<li><a>Dark</a></li>
</ul>
</details>
<div className="w-full max-w-xs text-left">
<div className="label">
<span className="label-text">Reviews sorting</span>
</div>
</div>
<details className="dropdown w-full max-w-xs mb-4 z" style={{ zIndex: 1 }}>
<summary className="btn w-full">Most recent first</summary>
<ul className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
<li><a>Most recent first</a></li>
<li><a>Random</a></li>
</ul>
</details>
</div>
{/* Second column */}
<div>
<div className="w-full max-w-xs text-left">
<div className="label">
<span className="label-text">Minimum rating</span>
</div>
</div>
<details className="dropdown w-full max-w-xs mb-4 z" style={{ zIndex: 1 }}>
<summary className="btn w-full">{minRating} stars</summary>
<ul className="p-2 shadow menu dropdown-content bg-base-100 rounded-box w-52">
{[1, 2, 3, 4, 5].map(star => (
<li key={star} onClick={() => setMinRating(star)}>
<a>{star} star{star > 1 ? 's' : ''}</a>
</li>
))}
</ul>
</details>
<div className="form-control w-full max-w-xs mb-4">
<div className="label">
<span className="label-text">Displaying first popup after (seconds)</span>
</div>
<input type="number" value={initialWaitTime} onChange={e => setInitialWaitTime(parseInt(e.target.value, 10))} placeholder="2" className="input input-bordered w-full max-w-xs" />
</div>
<div className="form-control w-full max-w-xs mb-4">
<div className="label">
<span className="label-text">Show popup every (seconds)</span>
</div>
<input type="number" placeholder="6" value={frequencyTime} onChange={e => setFrequencyTime(parseInt(e.target.value, 10))} className="input input-bordered w-full max-w-xs" />
</div>
<div className="form-control w-full max-w-xs">
<div className="label">
<span className="label-text">Duration of popup (seconds)</span>
</div>
<input type="number" placeholder="8" value={durationTime} onChange={e => setDurationTime(parseInt(e.target.value, 10))} className="input input-bordered w-full max-w-xs" />
</div>
</div>
</div>
<div className="w-full max-w-xl flex justify-center mt-4 px-3">
<button onClick={handleApplyClick} disabled={shouldFetch} className="btn w-full max-w-4xl bg-orange text-white">Apply</button>
</div>
<dialog id="my_modal_1" className="modal">
<div className="modal-box">
<h3 className="font-bold text-lg">Hello!</h3>
<p className="py-4">Press ESC key or click the button below to close</p>
<div className="modal-action">
<form method="dialog">
{/* if there is a button in form, it will close the modal */}
<button className="btn">Close</button>
</form>
</div>
</div>
</dialog>
</div>
</section>
<PopTrustToast key={key} placeId={placeId} shouldFetch={shouldFetch} setShouldFetch={setShouldFetch}
position={toastPosition} initialWaitTime={initialWaitTime} frequencyTime={frequencyTime} durationTime={durationTime} minRating={minRating} />
</main>
<Footer />
</>
);
}
PopTrustToast.tsx
"use client";
import React, { ReactEventHandler, use, useEffect, useState } from 'react';
import './PopTrustToast.css';
import toast, { ToastPosition } from 'react-hot-toast';
import { formatDistance } from 'date-fns';
const PopTrustToast = ({ placeId, shouldFetch, setShouldFetch, position, initialWaitTime, frequencyTime, durationTime, minRating }:
{
placeId: string, shouldFetch: boolean, setShouldFetch: (value: boolean) => void, position: ToastPosition,
initialWaitTime: number, frequencyTime: number, durationTime: number, minRating: number
}) => {
const [toastIndex, setToastIndex] = useState(0);
const [allowToasts, setAllowToasts] = useState(true); // New state to control toast display
const [messages, setMessages] = useState([]); // State to hold fetched messages
const epochToRelativeTime = (epoch: number) => {
const date = new Date(epoch * 1000); // Convert epoch to milliseconds
const now = new Date();
return formatDistance(date, now, { addSuffix: true });
};
const checkReviewStatus = async (placeId: String) => {
if (!placeId) return null;
const response = await fetch('http://localhost:3001/proxy?placeId=' + placeId, {
});
const data = await response.json();
return data;
};
const fetchReviews = async (placeId: String) => {
if (!placeId) return [];
try {
const response = await fetch(`api.xxxxx/reviews?uris%5B%5D=${placeId}&with_text_only=1&min_rating=${minRating}&page_length=50&order=random`);
const data = await response.json();
return data.result.data.map((review: { text: any; reviewer_name: any; reviewer_picture_url: any; id: { toString: () => any; }; rating: any; url: any; published_at: any }) => ({
text: review.text,
author: review.reviewer_name,
timeAgo: epochToRelativeTime(review.published_at),
src: review.reviewer_picture_url,
id: review.id.toString(),
stars: review.rating,
url: review.url
}));
} catch (error) {
console.error('Failed to fetch messages:', error);
return [];
}
};
// Fetch messages from API
useEffect(() => {
if (!placeId || !shouldFetch) return;
toast.dismiss(); // Dismiss all toasts before fetching new messages
setAllowToasts(false); // Prevent further toasts
setMessages([]); // Clear messages
toast.loading('Fetching reviews... This can take up to a minute', { duration: 9999999, position: 'bottom-center', style: { position: 'relative', bottom: '1rem' } });
const interval = setInterval(async () => {
const status = await checkReviewStatus(placeId);
const isComplete = status?.result?.data?.some((item: { profile_state: Object[]; }) => item.profile_state[0] === "complete" && item.profile_state[1] === 100);
if (isComplete) {
setAllowToasts(true);
setShouldFetch(false); // Set shouldFetch to false
toast.dismiss();
toast.success('Fetched reviews succesfully!', { position: 'bottom-center', style: { position: 'relative', bottom: '1rem' } })
clearInterval(interval);
fetchReviews(placeId).then(setMessages);
}
}, 3000);
return () => clearInterval(interval);
}, [placeId, shouldFetch]);
//First toast
useEffect(() => {
if (messages.length === 0) return;
const timeout = setTimeout(() => {
if (allowToasts) {
sendNotification(0);
}
}, initialWaitTime * 1000);
return () => clearTimeout(timeout);
}, [allowToasts, messages]);
let recurringInterval: NodeJS.Timeout;
useEffect(() => {
if (messages.length === 0) return;
// Dismiss all toasts when the location changes
toast.dismiss();
// Clear recurring interval
clearInterval(recurringInterval);
let toastIndex = 1;
const timeout = setTimeout(() => {
recurringInterval = setInterval(() => {
if (toastIndex < messages.length && allowToasts) {
sendNotification(toastIndex);
toastIndex++;
} else {
clearInterval(recurringInterval);
}
}, frequencyTime * 1000);
return () => clearInterval(recurringInterval);
}, initialWaitTime * 1000);
return () => clearTimeout(timeout);
}, [allowToasts, messages]);
const sendNotification = (toastIndex: number) => {
console.log("sending notification", toastIndex);
const message = messages[toastIndex];
console.log("position", position);
toast.custom((t) => <Message message={message} t={t} position={position} />, {
duration: durationTime * 1000,
position: position as ToastPosition,
id: toastIndex.toString(),
});
setToastIndex((prev) => prev + 1);
};
return <></>;
};
const Message = ({ message, t, position }: { message: any, t: any, position: ToastPosition }) => {
// Do some stuff
return (
// Toast HTML
);
};
export default PopTrustToast;