@backpackapp-io/react-native-toast
Version:
A toasting library for React Native. Built in features such as swipe to dismiss, multiple toasts, & no context power this library.
220 lines (193 loc) • 4.97 kB
text/typescript
import { useEffect, useState } from 'react';
import { DefaultToastOptions, DismissReason, Toast, ToastType } from './types';
const TOAST_LIMIT = 20;
export enum ActionType {
ADD_TOAST,
UPDATE_TOAST,
UPSERT_TOAST,
DISMISS_TOAST,
REMOVE_TOAST,
START_PAUSE,
END_PAUSE,
}
export type Action =
| {
type: ActionType.ADD_TOAST;
toast: Toast;
}
| {
type: ActionType.UPSERT_TOAST;
toast: Toast;
}
| {
type: ActionType.UPDATE_TOAST;
toast: Partial<Toast>;
}
| {
type: ActionType.DISMISS_TOAST;
toastId?: string;
reason: DismissReason;
}
| {
type: ActionType.REMOVE_TOAST;
toastId?: string;
reason?: DismissReason;
}
| {
type: ActionType.START_PAUSE;
time: number;
}
| {
type: ActionType.END_PAUSE;
time: number;
};
interface State {
toasts: Toast[];
pausedAt: number | undefined;
}
const toastTimeouts = new Map<Toast['id'], ReturnType<typeof setTimeout>>();
const addToRemoveQueue = (toastId: string, reason: DismissReason) => {
if (toastTimeouts.has(toastId)) {
return;
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId);
dispatch({
type: ActionType.REMOVE_TOAST,
toastId: toastId,
reason,
});
}, 1000);
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:
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
};
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
),
};
case ActionType.UPSERT_TOAST:
const { toast } = action;
return state.toasts.find((t) => t.id === toast.id)
? reducer(state, { type: ActionType.UPDATE_TOAST, toast })
: reducer(state, { type: ActionType.ADD_TOAST, toast });
case ActionType.DISMISS_TOAST:
const { toastId, reason } = action;
// ! Side effects ! - This could be execrated into a dismissToast() action, but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId, reason);
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id, reason);
});
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
visible: false,
dismissReason: reason,
}
: t
),
};
case ActionType.REMOVE_TOAST:
if (action.toastId === undefined) {
return {
...state,
toasts: [],
};
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
};
case ActionType.START_PAUSE:
return {
...state,
pausedAt: action.time,
};
case ActionType.END_PAUSE:
const diff = action.time - (state.pausedAt || 0);
return {
...state,
pausedAt: undefined,
toasts: state.toasts.map((t) => ({
...t,
pauseDuration: t.pauseDuration + diff,
})),
};
}
};
const listeners: Array<(state: State) => void> = [];
let memoryState: State = { toasts: [], pausedAt: undefined };
export const dispatch = (action: Action) => {
memoryState = reducer(memoryState, action);
listeners.forEach((listener) => {
listener(memoryState);
});
};
const defaultTimeouts: {
[key in ToastType]: number;
} = {
blank: 4000,
error: 4000,
success: 2000,
loading: Infinity,
};
export const useStore = (toastOptions: DefaultToastOptions = {}): State => {
const [state, setState] = useState<State>(memoryState);
useEffect(() => {
listeners.push(setState);
return () => {
const index = listeners.indexOf(setState);
if (index > -1) {
listeners.splice(index, 1);
}
};
}, [state]);
const mergedToasts = state.toasts
.filter(
(t) =>
toastOptions?.providerKey === undefined ||
t.providerKey === toastOptions?.providerKey ||
t.providerKey === 'PERSISTS'
)
.map((t) => ({
...toastOptions,
...toastOptions[t.type],
...t,
duration:
t.duration ||
toastOptions[t.type]?.duration ||
toastOptions?.duration ||
defaultTimeouts[t.type],
styles: {
...(t?.styles ?? {}),
},
}));
return {
...state,
toasts: mergedToasts,
};
};