Skip to content

Commit

Permalink
Refactor side-effects for removal
Browse files Browse the repository at this point in the history
Move them outside of the main store and into the useToaster
  • Loading branch information
timolins committed Dec 30, 2024
1 parent 607cf42 commit c7c4b82
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 58 deletions.
10 changes: 8 additions & 2 deletions site/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,9 @@ const Features = () => (
export default function Home() {
const [position, setPosition] = useState<ToastPosition>('top-center');
const [reverse, setReverse] = useState(false);
const { toasts: allToasts } = useToasterStore();
const { toasts: allToasts } = useToasterStore({
removeDelay: 2,
});

const shouldFade =
allToasts.filter((t) => t.visible).length && position.includes('top');
Expand Down Expand Up @@ -240,7 +242,11 @@ export default function Home() {
</div>
</header>
<SplitbeeCounter />
<Toaster position={position} reverseOrder={reverse} toastOptions={{}} />
<Toaster
position={position}
reverseOrder={reverse}
toastOptions={{ removeDelay: 10, style: { padding: 0 } }}
/>
<div className="container flex justify-end -mt-10 pointer-events-none">
<Butter2 className="transform translate-x-20" />
</div>
Expand Down
49 changes: 5 additions & 44 deletions src/core/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { DefaultToastOptions, Toast, ToastType } from './types';

const TOAST_LIMIT = 20;
const REMOVE_DELAY = 1000;

export enum ActionType {
ADD_TOAST,
Expand Down Expand Up @@ -49,31 +48,6 @@ interface State {
pausedAt: number | undefined;
}

const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();

const addToRemoveQueue = (toastId: string, removeDelay = REMOVE_DELAY) => {
if (toastTimeouts.has(toastId)) {
return;
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
});
}, removeDelay);

toastTimeouts.set(toastId, timeout);
};

const clearFromRemoveQueue = (toastId: string) => {
const timeout = toastTimeouts.get(toastId);
if (timeout) {
clearTimeout(timeout);
}
};

export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case ActionType.ADD_TOAST:
Expand All @@ -83,15 +57,12 @@ export const reducer = (state: State, action: Action): State => {
};

case ActionType.UPDATE_TOAST:
// ! Side effects !
if (action.toast.id) {
clearFromRemoveQueue(action.toast.id);
}

return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
t.id === action.toast.id
? { ...t, ...action.toast, dismissed: false, visible: true }
: t
),
};

Expand All @@ -104,24 +75,13 @@ export const reducer = (state: State, action: Action): State => {
case ActionType.DISMISS_TOAST:
const { toastId } = action;

// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(
toastId,
state.toasts.find((t) => t.id === toastId)?.removeDelay
);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id, toast.removeDelay);
});
}

return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
dismissed: true,
visible: false,
}
: t
Expand Down Expand Up @@ -211,6 +171,7 @@ export const useStore = (toastOptions: DefaultToastOptions = {}): State => {
...t.style,
},
}));

return {
...state,
toasts: mergedToasts,
Expand Down
1 change: 1 addition & 0 deletions src/core/toast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ const createToast = (
): Toast => ({
createdAt: Date.now(),
visible: true,
dismissed: false,
type,
ariaProps: {
role: 'status',
Expand Down
1 change: 1 addition & 0 deletions src/core/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export interface Toast {

createdAt: number;
visible: boolean;
dismissed: boolean;
height?: number;
}

Expand Down
36 changes: 36 additions & 0 deletions src/core/use-toaster.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,26 @@ const startPause = () => {
});
};

const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();

export const REMOVE_DELAY = 1000;

const addToRemoveQueue = (toastId: string, removeDelay = REMOVE_DELAY) => {
if (toastTimeouts.has(toastId)) {
return;
}

const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
});
}, removeDelay);

toastTimeouts.set(toastId, timeout);
};

export const useToaster = (toastOptions?: DefaultToastOptions) => {
const { toasts, pausedAt } = useStore(toastOptions);

Expand Down Expand Up @@ -84,6 +104,22 @@ export const useToaster = (toastOptions?: DefaultToastOptions) => {
[toasts]
);

useEffect(() => {
// Add dismissed toasts to remove queue
toasts.forEach((toast) => {
if (toast.dismissed) {
addToRemoveQueue(toast.id, toast.removeDelay);
} else {
// If toast becomes visible again, remove it from the queue
const timeout = toastTimeouts.get(toast.id);
if (timeout) {
clearTimeout(timeout);
toastTimeouts.delete(toast.id);
}
}
});
}, [toasts]);

return {
toasts,
handlers: {
Expand Down
26 changes: 14 additions & 12 deletions test/toast.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import {
} from '@testing-library/react';

import toast, { resolveValue, Toaster, ToastIcon } from '../src';
import { TOAST_EXPIRE_DISMISS_DELAY, defaultTimeouts } from '../src/core/store';
import { defaultTimeouts } from '../src/core/store';
import { REMOVE_DELAY } from '../src/core/use-toaster';

beforeEach(() => {
// Tests should run in serial for improved isolation
Expand Down Expand Up @@ -70,7 +71,7 @@ test('close notification', async () => {

fireEvent.click(await screen.findByRole('button', { name: /close/i }));

waitTime(TOAST_EXPIRE_DISMISS_DELAY);
waitTime(REMOVE_DELAY);

expect(screen.queryByText(/example/i)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -180,7 +181,9 @@ test('error toast with custom duration', async () => {

expect(screen.queryByText(/error/i)).toBeInTheDocument();

waitTime(TOAST_DURATION + TOAST_EXPIRE_DISMISS_DELAY);
waitTime(TOAST_DURATION);

waitTime(REMOVE_DELAY);

expect(screen.queryByText(/error/i)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -211,7 +214,6 @@ test('different toasts types with dismiss', async () => {
icon: <span>ICON</span>,
});
});

let loadingToastId: string;
act(() => {
loadingToastId = toast.loading('Loading!');
Expand All @@ -223,25 +225,24 @@ test('different toasts types with dismiss', async () => {
expect(screen.queryByText('✅')).toBeInTheDocument();
expect(screen.queryByText('ICON')).toBeInTheDocument();

const successDismissTime =
defaultTimeouts.success + TOAST_EXPIRE_DISMISS_DELAY;
waitTime(defaultTimeouts.success);

waitTime(successDismissTime);
waitTime(REMOVE_DELAY);

expect(screen.queryByText(/success/i)).not.toBeInTheDocument();
expect(screen.queryByText(/error/i)).toBeInTheDocument();

waitTime(
defaultTimeouts.error + TOAST_EXPIRE_DISMISS_DELAY - successDismissTime
);
waitTime(defaultTimeouts.error);

waitTime(REMOVE_DELAY);

expect(screen.queryByText(/error/i)).not.toBeInTheDocument();

act(() => {
toast.dismiss(loadingToastId);
});

waitTime(TOAST_EXPIRE_DISMISS_DELAY);
waitTime(REMOVE_DELAY);

expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
});
Expand Down Expand Up @@ -313,7 +314,8 @@ test('pause toast', async () => {

fireEvent.mouseLeave(toastElement);

waitTime(2000);
waitTime(1000);
waitTime(1000);

expect(toastElement).not.toBeInTheDocument();
});

0 comments on commit c7c4b82

Please sign in to comment.