gravity-ui / rfc

Gravity RFC is a process for proposing and implementing changes in our ecosystem
MIT License
3 stars 0 forks source link

The incompatibility of different Toaster APIs (hooks vs singleton) #15

Open icekimi23 opened 3 days ago

icekimi23 commented 3 days ago

Objective

In the gravity-ui library, there are several ways to invoke a toast (a notification window at the edge of the screen): through the useToaster hook or, where hooks are not applicable, through the Toaster singleton.

However, when using the singleton approach, a problem may arise where components rendered within such a toast do not have access to various application providers, because these toasts are mounted in a different root.

Using both approaches in a single project can lead to issues with toasts overlapping each other, as each method has its own stack of toasts. (An example is shown in the video.)

Solution Proposal

Create a universal API with methods that do not conflict with each other. This could be implemented similar to Redux, where a singleton is passed into a provider and can also be used externally.

Rough implementation

Click to expand ```typescript import React, { createContext, useContext, useState, useMemo, useEffect } from "react"; export const ToasterContext = createContext(null); ToasterContext.displayName = "ToasterContext"; export const ToasterProvider = ({ toaster, children }) => { const [toasts, setToasts] = useState([]); useEffect(() => { const updateToasts = (toasts) => { setToasts((prev) => [...toasts]); }; toaster.on(updateToasts); return () => { toaster.off(updateToasts); }; }, [toaster]); return ( <> {children}
{toasts.map((toast) => (
{toast}
))}
); }; export const useToaster = () => { const toaster = useContext(ToasterContext); return useMemo(() => toaster, [toaster]); }; class Toaster { constructor() { this.toasts = []; this.listeners = []; } add(toast) { this.toasts.push(toast); for (const listener of this.listeners) { listener(this.toasts); } } on(fn) { this.listeners.push(fn); } off(fn) { this.listeners = this.listeners.filter((listener) => listener !== fn); } } const toaster = new Toaster(); function SomeComponent() { const toasterFromHook = useToaster(); const handleClick = () => { toasterFromHook.add("a new fresh toast"); }; return ; } setTimeout(() => { toaster.add('toast from setTimeout'); }, 3000); export default function App() { return (
); } ```

The main idea is to create an object that maintains the state of toasts and allows subscribing to its changes. Essentially, it's the same concept as redux + react-redux. This way, the object is created once in the service and then used as needed.

This is my initial thought, but if someone suggests something simpler, I'd be happy to take a look :)

Definition of done

A pull request is made with a universal, non-conflicting API.

korvin89 commented 3 days ago

cc @ValeraS @ogonkov