Documentation Index
Fetch the complete documentation index at: https://mintlify.com/reatom/reatom/llms.txt
Use this file to discover all available pages before exploring further.
Overview
The withSuspenseRetry extension wraps an async action to automatically retry it when a Promise is thrown (suspension). It keeps retrying until the action completes successfully or throws a non-Promise error.
This is particularly useful when working with atoms that use suspense patterns, where you want actions to wait for suspended data to resolve before continuing.
Type Signature
function withSuspenseRetry<T extends Action<unknown[], Promise<unknown>>>(): Ext<T>
Warning
Be careful with non-idempotent operations inside the action body, as they may be executed multiple times during retries. Plan your execution logic carefully to handle potential retries safely.
Return Value
Returns the same action with automatic suspension retry behavior.
Examples
Basic Retry
import { action, atom } from '@reatom/core'
import { withSuspenseRetry, withSuspenseInit } from '@reatom/core/extensions'
import { wrap } from '@reatom/core/methods'
const suspenseAtom = atom(async () => {
await wrap(sleep())
return 1
}).extend(withSuspenseInit())
const act = action(async () => {
const result = suspenseAtom() // Will throw promise first time
return result
}).extend(withSuspenseRetry())
const result = await wrap(act())
console.log(result) // → 1 (retried automatically)
Fetch User Books After User Loads
const user = atom(async () => {
const res = await fetch('/api/user')
return res.json()
}).extend(withSuspenseInit())
const fetchUserBooks = action(async () => {
const userData = user() // Suspends until user is loaded
const response = await wrap(fetch(`/api/users/${userData.id}/books`))
return await wrap(response.json())
}).extend(withSuspenseRetry())
// Call immediately - will retry until user atom is ready
const books = await wrap(fetchUserBooks())
console.log(books)
Chained Suspense Dependencies
const config = atom(async () => {
await sleep(100)
return { apiUrl: 'https://api.example.com' }
}).extend(withSuspenseInit())
const auth = atom(async () => {
await sleep(50)
return { token: 'abc123' }
}).extend(withSuspenseInit())
const fetchData = action(async (endpoint: string) => {
const cfg = config() // Suspends until config loads
const authData = auth() // Suspends until auth loads
const res = await wrap(fetch(`${cfg.apiUrl}${endpoint}`, {
headers: { Authorization: `Bearer ${authData.token}` },
}))
return await wrap(res.json())
}).extend(withSuspenseRetry())
// Automatically waits for both config and auth
const data = await wrap(fetchData('/users'))
With Error Handling
const resource = atom(async () => {
await sleep()
return { id: 1, name: 'Resource' }
}).extend(withSuspenseInit())
const processResource = action(async () => {
try {
const res = resource() // May suspend
if (res.id < 0) {
throw new Error('Invalid resource')
}
return `Processed: ${res.name}`
} catch (error) {
// Non-promise errors are not retried
if (!(error instanceof Promise)) {
console.error('Processing failed:', error)
throw error
}
throw error // Re-throw promise for retry
}
}).extend(withSuspenseRetry())
const result = await wrap(processResource())
console.log(result) // → "Processed: Resource"
Multiple Suspended Atoms
const userAtom = atom(async () => {
await sleep(100)
return { id: 1, name: 'John' }
}).extend(withSuspenseInit())
const settingsAtom = atom(async () => {
await sleep(50)
return { theme: 'dark' }
}).extend(withSuspenseInit())
const initializeApp = action(async () => {
const user = userAtom() // Suspends
const settings = settingsAtom() // Suspends
return {
user,
settings,
initialized: true,
}
}).extend(withSuspenseRetry())
// Retries until both atoms are ready
const app = await wrap(initializeApp())
console.log(app.initialized) // → true
How It Works
// Simplified implementation
withSuspenseRetry = () =>
withActionMiddleware(() => async (next, ...params) => {
while (true) {
try {
return await wrap(next(...params))
} catch (error) {
if (error instanceof Promise) {
await wrap(error) // Wait for promise to resolve
// Then retry
} else {
throw error // Non-promise errors are thrown
}
}
}
})
Use Cases
Dependent Data Loading
const userId = atom(async () => {
// Load from URL or storage
return getUserIdFromAuth()
}).extend(withSuspenseInit())
const loadUserProfile = action(async () => {
const id = userId() // Suspends until user ID is available
const res = await wrap(fetch(`/api/users/${id}`))
return await wrap(res.json())
}).extend(withSuspenseRetry())
Sequential Async Operations
const database = atom(async () => {
const db = await openDatabase()
return db
}).extend(withSuspenseInit())
const saveData = action(async (data: Data) => {
const db = database() // Suspends until DB is ready
await db.save(data)
return { success: true }
}).extend(withSuspenseRetry())
React Component Actions
const config = atom(async () => {
const res = await fetch('/api/config')
return res.json()
}).extend(withSuspenseInit())
const submitForm = action(async (formData: FormData) => {
const cfg = config() // Suspends if config not loaded
const res = await fetch(cfg.submitUrl, {
method: 'POST',
body: formData,
})
return res.json()
}).extend(withSuspenseRetry())
function MyForm() {
const handleSubmit = useAction(submitForm)
return (
<form onSubmit={e => {
e.preventDefault()
handleSubmit(new FormData(e.target))
}}>
{/* form fields */}
</form>
)
}
Important Considerations
Non-Idempotent Operations
// ⚠️ BAD - Side effect runs multiple times
const badAction = action(async () => {
incrementCounter() // Runs on every retry!
const data = suspendedAtom()
return data
}).extend(withSuspenseRetry())
// ✅ GOOD - Side effect runs after suspension resolved
const goodAction = action(async () => {
const data = suspendedAtom() // May suspend
incrementCounter() // Only runs after suspension
return data
}).extend(withSuspenseRetry())
Error Recovery
const retryableAction = action(async () => {
const resource = suspendedAtom()
// Non-promise errors stop retrying
if (!resource.valid) {
throw new Error('Invalid resource')
}
return processResource(resource)
}).extend(withSuspenseRetry())
Nested Suspense
const outer = atom(async () => {
await sleep(100)
return 'outer'
}).extend(withSuspenseInit())
const inner = atom(async () => {
await sleep(50)
return 'inner'
}).extend(withSuspenseInit())
const process = action(async () => {
const o = outer() // First suspension point
const i = inner() // Second suspension point
return `${o}-${i}`
}).extend(withSuspenseRetry())
// Retries until both are ready
await wrap(process()) // → "outer-inner"