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) => (
-
+
)}