diff --git a/dev/components/Types/index.tsx b/dev/components/Types/index.tsx index aed28ae..96ca679 100644 --- a/dev/components/Types/index.tsx +++ b/dev/components/Types/index.tsx @@ -72,8 +72,11 @@ toast.promise(promise, { action: () => toast.promise<{ name: string }>( () => - new Promise((resolve) => { + new Promise((resolve, reject) => { setTimeout(() => { + const random50Percent = Math.floor(Math.random() * 2) + if (random50Percent > 0) + reject(new Error('Something\'s not right!')) resolve({ name: 'Solid Sonner' }) }, 1500) }), @@ -86,6 +89,31 @@ toast.promise(promise, { }, ), }, + { + name: 'Loading', + snippet: `const promise = () => new Promise((resolve) => setTimeout(resolve, 800)); + +toast.loading('Uploading...', { id: 'form' }); +await promise(); +toast.loading('Saving...', { id: 'form'}); +await promise(); +toast.success('Success!', { id: 'form' }); +`, + action: async () => { + const idAlphabet = 'abcdefghijklmnopqrstuvwxyz1234567890' + const getRandomIndex = () => Math.floor(Math.random() * idAlphabet.length) + const toastId = idAlphabet[getRandomIndex()]! + idAlphabet[getRandomIndex()]! + idAlphabet[getRandomIndex()]! + + const promise = () => new Promise((resolve) => { + setTimeout(resolve, 1000) + }) + toast.loading('Uploading...', { id: toastId }) + await promise() + toast.loading('Saving...', { id: toastId }) + await promise() + toast.success('Success!', { id: toastId }) + }, + }, { name: 'Custom', snippet: 'toast(
A custom toast with default styling
)', diff --git a/src/index.tsx b/src/index.tsx index db476d7..9e808da 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -6,9 +6,9 @@ * https://github.com/emilkowalski/sonner/blob/main/src/index.tsx */ import './styles.css' -import type { Component, ValidComponent } from 'solid-js' -import { Dynamic } from 'solid-js/web' +import type { Component } from 'solid-js' import { For, Show, createEffect, createSignal, mergeProps, on, onCleanup, onMount } from 'solid-js' +import { createStore, produce, reconcile } from 'solid-js/store' import { Loader, getAsset } from './assets' import type { ExternalToast, HeightT, Position, ToastProps, ToastT, ToastToDismiss, ToasterProps } from './types' import { ToastState, toast } from './state' @@ -340,12 +340,8 @@ const Toast: Component = (props) => { <>
- } - > - props.toast.icon)) || props.icons?.loading || getLoadingIcon()) as ValidComponent} /> - + {props.toast.promise || (props.toast.type === 'loading' && !props.toast.icon) ? props.toast.icon || getLoadingIcon() : null} + {props.toast.type !== 'loading' ? props.toast.icon || props.icons?.[toastType() as unknown as keyof typeof props.icons] || getAsset(toastType()!)!() : null}
@@ -427,10 +423,16 @@ const Toaster: Component = (props) => { dir: getDocumentDirection(), }, props) as ToasterProps & { position: Position; hotkey: string[]; visibleToasts: number } - const [toasts, setToasts] = createSignal([]) + /** + * Use a store instead of a signal for fine-grained reactivity. + * All the setters only have to change the deepest part of the tree + * to maintain referential integrity when rendered in the DOM. + */ + const [toastsStore, setToastsStore] = createStore<{ toasts: ToastT[] }>({ toasts: [] }) + const possiblePositions = () => { return Array.from( - new Set([propsWithDefaults.position].concat(toasts().filter(toast => toast.position).map(toast => toast.position as Position))), + new Set([propsWithDefaults.position].concat(toastsStore.toasts.filter(toast => toast.position).map(toast => toast.position as Position))), ) } const [heights, setHeights] = createSignal([]) @@ -449,29 +451,31 @@ const Toaster: Component = (props) => { : 'light' : 'light', ) - const removeToast = (toast: ToastT) => setToasts(toasts => toasts.filter(({ id }) => id !== toast.id)) + const removeToast = (toast: ToastT) => setToastsStore('toasts', toasts => toasts.filter(({ id }) => id !== toast.id)) onMount(() => { const unsub = ToastState.subscribe((toast) => { if ((toast as ToastToDismiss).dismiss) { - setToasts(toasts => toasts.map(t => (t.id === toast.id ? { ...t, delete: true } : t))) + setToastsStore('toasts', produce((_toasts) => { + _toasts.forEach((t) => { + if (t.id === toast.id) + t.delete = true + }) + })) return } - setToasts((toasts) => { - const indexOfExistingToast = toasts.findIndex(t => t.id === toast.id) - - // Update the toast if it already exists - if (indexOfExistingToast !== -1) { - return [ - ...toasts.slice(0, indexOfExistingToast), - { ...toasts[indexOfExistingToast], ...toast }, - ...toasts.slice(indexOfExistingToast + 1), - ] - } + // Update (Fine-grained) + const changedIndex = toastsStore.toasts.findIndex(t => t.id === toast.id) + if (changedIndex !== -1) { + setToastsStore('toasts', [changedIndex], reconcile(toast)) + return + } - return [toast, ...toasts] - }) + // Insert (Fine-grained) + setToastsStore('toasts', produce((_toasts) => { + _toasts.unshift(toast) + })) }) onCleanup(() => { @@ -504,7 +508,7 @@ const Toaster: Component = (props) => { createEffect(() => { // Ensure expanded is always false when no toasts are present / only one left - if (toasts().length <= 1) + if (toastsStore.toasts.length <= 1) setExpanded(false) }) @@ -549,7 +553,7 @@ const Toaster: Component = (props) => { ) return ( - 0}> + 0}> {/* Remove item from normal navigation flow, only available via hotkey */}
@@ -603,36 +607,34 @@ const Toaster: Component = (props) => { onPointerUp={() => setInteracting(false)} > (!toast.position && index() === 0) || toast.position === position) - }> + toastsStore.toasts.filter(toast => (!toast.position && index() === 0) || toast.position === position)}> {(toast, index) => ( - + )}