Defer pending toast in Toast.promise() via loadingDelay

When the AppView indexes a new post quickly, briefly flashing the
spinner before it morphs into the success toast feels glitchy. Add a
`loadingDelay` option to Toast.promise() so the loading toast is only
rendered if the promise hasn't settled within the delay; otherwise we
jump straight to success/error.

Default is 0 (immediate, matching the existing behaviour). The composer
opts in to a 500ms delay so the common fast path shows only the success
toast.
This commit is contained in:
Claude
2026-05-17 10:52:43 +00:00
parent 1103132092
commit 31aa4a4f40
3 changed files with 53 additions and 6 deletions
+26 -3
View File
@@ -83,6 +83,13 @@ type PromiseToastOptions<T> = Omit<BaseToastOptions, 'type'> & {
loading: React.ReactNode
success: React.ReactNode | ((data: T) => React.ReactNode)
error?: React.ReactNode | ((err: unknown) => React.ReactNode)
/**
* Delay (ms) before the loading toast is shown. If the promise settles
* before this delay elapses, the loading toast is skipped entirely and the
* success/error toast appears directly. Defaults to 0 (loading shown
* immediately).
*/
loadingDelay?: number
}
/**
@@ -92,9 +99,17 @@ type PromiseToastOptions<T> = Omit<BaseToastOptions, 'type'> & {
*/
export function promise<T>(
input: Promise<T>,
{loading, success, error, ...options}: PromiseToastOptions<T>,
{
loading,
success,
error,
loadingDelay = 0,
...options
}: PromiseToastOptions<T>,
): Promise<T> {
const id = nanoid()
let settled = false
let loadingShown = false
const render = (
content: React.ReactNode,
@@ -114,19 +129,27 @@ export function promise<T>(
)
}
render(loading, 'pending')
const timer = setTimeout(() => {
if (settled) return
loadingShown = true
render(loading, 'pending')
}, loadingDelay)
return input.then(
data => {
settled = true
clearTimeout(timer)
const content = typeof success === 'function' ? success(data) : success
render(content, 'success')
return data
},
err => {
settled = true
clearTimeout(timer)
if (error !== undefined) {
const content = typeof error === 'function' ? error(err) : error
render(content, 'error')
} else {
} else if (loadingShown) {
sonner.dismiss(id)
}
throw err
+26 -3
View File
@@ -83,6 +83,13 @@ type PromiseToastOptions<T> = Omit<BaseToastOptions, 'type'> & {
loading: React.ReactNode
success: React.ReactNode | ((data: T) => React.ReactNode)
error?: React.ReactNode | ((err: unknown) => React.ReactNode)
/**
* Delay (ms) before the loading toast is shown. If the promise settles
* before this delay elapses, the loading toast is skipped entirely and the
* success/error toast appears directly. Defaults to 0 (loading shown
* immediately).
*/
loadingDelay?: number
}
/**
@@ -92,9 +99,17 @@ type PromiseToastOptions<T> = Omit<BaseToastOptions, 'type'> & {
*/
export function promise<T>(
input: Promise<T>,
{loading, success, error, ...options}: PromiseToastOptions<T>,
{
loading,
success,
error,
loadingDelay = 0,
...options
}: PromiseToastOptions<T>,
): Promise<T> {
const id = nanoid()
let settled = false
let loadingShown = false
const render = (
content: React.ReactNode,
@@ -115,19 +130,27 @@ export function promise<T>(
)
}
render(loading, 'pending')
const timer = setTimeout(() => {
if (settled) return
loadingShown = true
render(loading, 'pending')
}, loadingDelay)
return input.then(
data => {
settled = true
clearTimeout(timer)
const content = typeof success === 'function' ? success(data) : success
render(content, 'success')
return data
},
err => {
settled = true
clearTimeout(timer)
if (error !== undefined) {
const content = typeof error === 'function' ? error(err) : error
render(content, 'error')
} else {
} else if (loadingShown) {
sonner.dismiss(id)
}
throw err
+1
View File
@@ -1035,6 +1035,7 @@ export const ComposePost = ({
})()
Toast.promise(appViewReady, {
loadingDelay: 500,
loading: (
<Toast.Outer>
<Toast.Icon />