Creating an object with hooks as property

Viewed 64

Description:
I am using Radix-UI and I want to create a service for creating toasts: https://www.radix-ui.com/docs/primitives/components/toast

Problem:
I am unable to change the statte of the open-hook inside the object

How it works:

  • Any random component from anywhere in the app can add toasts to the store
  • The Toastr.tsx watches the store toasts
  • whenever a new object/ToastrItem is added it renders the toast
  • After 5 seconds (default value) onOpenChangeis called to set open=false through a hook. This does not work

Toastr.tsx

const Toastr = () => {
    const { toasts, setToasts } = useToastrStore();
    const eventDateRef = React.useRef(new Date());
    const timerRef = React.useRef(0);

    React.useEffect(() => {
        console.log(toasts);
        return () => clearTimeout(timerRef.current);
    }, []);

    React.useEffect(() => {
        console.log('TOASTS:');
        console.log(toasts);
    }, [toasts]);

    return (
        <ToastProvider swipeDirection='right'>
            {toasts &&
                toasts.length > 0 &&
                isArray(toasts) &&
                toasts.map((toast: ToastrItem, idx: number) => {
                    return (
                        <Toast
                            open={toast.open}
                            onOpenChange={(openStatus: boolean) => {
                              //This does not work
                              toast.setOpen(openStatus)
                            }}
                            key={idx}
                        >
                            <ToastTitle>
                                {toast.title}
                                <h1>{toast.open}</h1>
                            </ToastTitle>
                            <ToastDescription asChild>
                                <time dateTime={eventDateRef.current.toISOString()}>{'TEEEEEEEEEEEEEEEEEEEEEEEEEEXT'}</time>
                            </ToastDescription>
                            <ToastAction asChild altText='Goto schedule to undo'>
                                <Button variant='green' size='small'>
                                    Close
                                </Button>
                            </ToastAction>
                        </Toast>
                    );
                })}
            <ToastViewport />
        </ToastProvider>
    );
};

The piece of code that does not work is:

                            onOpenChange={(openStatus: boolean) => {
                              //This does not work
                              toast.setOpen(openStatus)
                            }}

My theory is that it is because of the generator i am using:

getToast.tsx

export const getToast = () => {
    const toast = new ToastObject() as ToastrItem;
    const [open, setOpen] = useState(false);
    toast.open = open;
    toast.setOpen = setOpen;
    return toast;
};
class ToastObject implements ToastrItem {
    id: string;
    title: string;
    description: string;
    variant: ToastrVariant;
    open: boolean;
    setOpen: (param: boolean) => void;

    constructor() {
        this.id = nanoid();
        this.title = 'test';
        this.description = 'description';
        this.variant = ToastrVariant.GOOD;
        this.open = true;
        this.setOpen = () => false;
    }
}

ToastrItem.tsx

export interface ToastrItem {
    id: string;
    title: string;
    description: string;
    variant: ToastrVariant;
    open: boolean;
    setOpen: (parameter: boolean) => any;
}
1 Answers

What are you using getToast()?

I think I see the problem without seeing the usage. React state is immutable and can only be changed by calling setState(). This means that if you modify a value within the current state, React doesn't know that the state has actually changed. You need to instead copy the original state, update what you need, and then call setState again.

Since you're using zustand, I'll show how you would accomplish this using that. I haven't actually tested this code, but it's the correct paradigm for what you want to do.

const useToastrStore = create((set) => ({
    toasts: [],
    // addToast will update the toasts state by appending the new toast
    addToast: (toast) => set(state => ({toasts: [...state.toasts, toast]})),
    // setOpen takes a toastIndex and an open status
    setOpen: (toastIndex, openStatus) => {
        set(state => ({
                ...state,
                // map through all toasts, find the one that matches toastIndex, and change it's open status
                toasts: state.toasts.map((toast, index) => index === toastIndex ? {
                    ...toast,
                    open: openStatus
                } : {...toast})
            })
        )
    }
}))

const Toastr = () => {
    const { toasts, addToast, setOpen } = useToastrStore();

    return (
        <ToastProvider swipeDirection='right'>
            {toasts.map((toast: ToastrItem, idx: number) => (
                <Toast
                    open={toast.open}
                    onOpenChange={(openStatus: boolean) => {
                      //This does not work
                      setOpen(idx, openStatus)
                    }}
                    key={idx}
                >
                    
                </Toast>
            )}
        </ToastProvider>
    );
};
Related