Open ADTC opened 3 months ago
What's even weirder is that in order to create multiple toasts, I have to create an array of Toast components? 🤯
It's funny that the only thing useful about this toaster is its styling.
Anyhow, here is how I solved it with jotai and tailwind. This ain't optimized but at least has more functionality than theirs.
import { Icon } from "@iconify-icon/react";
import * as Toast from "@radix-ui/react-toast";
import { useAtom } from "jotai";
import { useEffect, useMemo, useRef } from "react";
const Toaster = () => {
const [{ list }] = useAtom(toastAtom);
return (
<Toast.Provider swipeDirection="right">
{list.map((props) => (
<SingleToast {...props} key={props.id} />
))}
<Toast.Viewport className="fixed bottom-0 right-0 flex flex-col p-8 gap-3 w-96 max-w-screen m-0 list-none !z-50 outline-none " />
</Toast.Provider>
);
};
const modes: { [type: string]: { textColor: string; borderColor: string } } = {
positive: { textColor: "text-positive", borderColor: "border-positive" },
negative: { textColor: "text-negative", borderColor: "border-negative" },
info: { textColor: "text-info", borderColor: "border-info" },
};
const SingleToast = ({
id,
open = true,
title = "",
subtitle = "",
mode = "info",
timer = 3000,
infinite = false,
}: {
id?: number;
open?: boolean;
title: string;
subtitle: string;
mode?: string;
timer?: number;
infinite?: boolean;
}) => {
const [_, setToast] = useAtom(toastAtom);
const timerRef = useRef<NodeJS.Timeout | undefined>();
const removeToast = () => {
setToast((state) => {
state.list = state.list.filter((toast) => toast.id != id);
});
timerRef.current && clearTimeout(timerRef.current);
};
const setTimer = () => {
timerRef.current = setTimeout(() => {
removeToast();
clearTimeout(timerRef.current);
}, timer + 1000);
};
useEffect(() => {
!infinite && setTimer();
}, []);
return (
<Toast.Root
className={`${modes[mode].borderColor} border border-accent/50 bg-light dark:bg-dark rounded-md shadow-[hsl(206_22%_7%_/_35%)_0px_10px_38px_-10px,_hsl(206_22%_7%_/_20%)_0px_10px_20px_-15px] p-[15px] grid [grid-template-areas:_'title_action'_'description_action'] grid-cols-[auto_max-content] gap-x-[15px] items-center `}
duration={Infinity}
>
<Toast.Title className={`font-bold ${modes[mode].textColor}`}>{title}</Toast.Title>
<Toast.Description className="text-base">{subtitle}</Toast.Description>
<Toast.Action className="[grid-area:_action]" asChild altText="Dismiss">
<Toast.Close
aria-label="Close"
className={`border rounded-full h-8 w-8 flex justify-center items-center ${modes[mode].borderColor}`}
onClick={removeToast}
>
<Icon icon="ph:x-thin" className={`text-xl ${modes[mode].textColor}`} />
</Toast.Close>
</Toast.Action>
</Toast.Root>
);
};
export default Toaster;
export const useToast = () => {
const [_, setToast] = useAtom(toastAtom);
return ({ timer = 3000, ...toast }: Toast) => {
const id = Date.now() + Math.random();
setToast((state) => {
state.list = [...state.list, { id, timer, ...toast }];
});
};
};
import { atomWithImmer } from "jotai-immer";
export const toastAtom = atomWithImmer<{ list: Array<Toast> }>({ list: [] });
interface Toast {
id?: number;
open?: boolean;
title: string;
subtitle: string;
timer?: number;
infinite?: boolean;
mode?: "info" | "positive" | "negative";
}
function App({ children }: PropsWithChildren) {
const toast = useToast();
useEffect(() => {
toast({ title: "Failed", subtitle: "Something went wrong!", mode: "negative" });
toast({ title: "Success", subtitle: "Hurray!", mode: "positive", timer: 8000 });
toast({ title: "Info", subtitle: "Cool info!", timer: 12000 });
toast({ title: "Infinite", subtitle: "I'll always be here!", infinite: true });
}, []);
return (
<RadixTheme>
{children}
<Toaster />
</RadixTheme>
);
}
export default App;
Documentation
Radix Toast documentation shows very weirdly how to build a single toast surrounding a button which opens it.
That's it. Nothing more. How I'm supposed to build an application-wide toasting system from this, I don't know.
The docs look extremely lacking in clarity, and I'm surprised no one has raised this concern yet.
Relevant Radix Component(s)
The Radix doc shows how to build a
Toaster
from its parts, that envelopes a button which changes a state variable, which then does the toasting. It seems like its purpose is to just generate that toast on that button. So, it's not portable. What if I want to open a toast from an error state after sending a request to an API? I don't know how to derive that from this documentation.You could say, have a button that calls the API, and wrap this button with
<Toast.Provider>
but how does that make sense? I have to wrap every button that needs a toast with it? How about toasting for events that are not triggered by buttons? Like a WebRTC push mesage?What's even weirder is that in order to create multiple toasts, I have to create an array of Toast components? 🤯
Examples from other doc sites
Please compare to these where it shows how to toast from anywhere, in any JavaScript. You plugin a
Toaster
then you justtoast
. The example is a button click, but I can very easily derive how to toast from an error state after sending an API request. Multiple toasts? Just calltoast
again and again. Avoid duplicates? Just add ID to your toasts. Want duplicates? Don't add ID to your toasts.Is this simplicity even possible to replicate in Radix Toast? Or am I just too dumb? 😂