Avatars
Fetch and display user avatar URLs with built-in batching, caching, and image preloading.
Avatar URLs are signed and expire after 15 minutes. The AvatarApiClient handles this transparently
— requests are batched into a single API call per 100ms window, and results are cached for 10 minutes
(the margin keeps a URL handed out just before eviction valid by the time the browser fetches it). The
useAvatar hook wraps this for React consumers and preloads the image before updating state to prevent
flickering.
The cache is mirrored to sessionStorage, so a page refresh reuses the URLs it already holds instead of
re-signing them. That matters beyond the saved API call: every signing produces a different URL, which is
a different browser-cache key, so re-signing forces the image bytes to download again. Persistence is
per-tab and best-effort — where storage is unavailable or full, the cache silently stays in memory.
React Hook: useAvatar
Returns the avatar URL for a single user. Reads from cache synchronously on mount, fetches if needed, and preloads the image before exposing the URL.
import { useAvatar } from '@skedulo/horizon-core'
const UserCard = ({ userId }: { userId: string }) => {
const avatarUrl = useAvatar(userId)
return (
<div>
{avatarUrl ? (
<img src={avatarUrl} alt="User avatar" />
) : (
<span>No avatar</span>
)}
</div>
)
}
Parameters
| Parameter | Type | Description |
|---|---|---|
userId | string | undefined | The user ID to fetch an avatar for |
size | 'large' | 'thumbnail' | Avatar size (default: 'large') |
Return Value
| Type | Description |
|---|---|
string | null | The avatar URL, or null if not loaded, not found, or on error |
Thumbnail example
import { useAvatar } from '@skedulo/horizon-core'
const SmallAvatar = ({ userId }: { userId: string }) => {
const avatarUrl = useAvatar(userId, 'thumbnail')
return avatarUrl ? <img src={avatarUrl} alt="" /> : null
}
Avatar API Client
For non-React code or when you need to fetch avatars for multiple users at once, use the
AvatarApiClient directly via apiClient.avatar.
import { useApiClient } from '@skedulo/horizon-core'
const apiClient = useApiClient()
// Fetch avatars for multiple users in one batched call
const avatars = await apiClient.avatar.getAvatars([
'user-1',
'user-2',
'user-3',
])
// => { 'user-1': 'https://...', 'user-2': 'https://...', 'user-3': null }
// Read from cache without triggering a fetch
const cached = apiClient.avatar.getCachedAvatar('user-1')
// Clear the entire cache (e.g. after a profile picture upload)
apiClient.avatar.clearCache()
// Clear cache for a single user
apiClient.avatar.clearSingleCache('user-1')
Methods
| Method | Signature | Description |
|---|---|---|
getAvatars | (userIds: string[], size?: AvatarSize) => Promise<Record<string, string | null>> | Fetch avatars with automatic batching and caching |
getCachedAvatar | (userId: string, size?: AvatarSize) => string | null | undefined | Synchronous cache read (undefined = not cached or expired) |
clearCache | () => void | Clear all cached avatars, in memory and in sessionStorage |
clearSingleCache | (userId: string, size?: AvatarSize) => void | Clear cache for one user (all sizes if omitted) |
getCacheSize | () => number | Number of unexpired entries currently cached |
Batching behaviour
Requests made within a 100ms window are automatically batched into a single API call. Batches are
capped at 50 user IDs per request — larger sets are split into multiple calls. Cache is per-size, so
'large' and 'thumbnail' URLs for the same user are stored independently.
Expiry
Entries live 10 minutes. An expired entry reads as undefined from getCachedAvatar and is refetched
by getAvatars, so a stale signed URL is never handed to an <img>. Entries that expired while a tab
was in the background are dropped when the persisted cache is read back on the next page load.