Horizon Framework

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

ParameterTypeDescription
userIdstring | undefinedThe user ID to fetch an avatar for
size'large' | 'thumbnail'Avatar size (default: 'large')

Return Value

TypeDescription
string | nullThe 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

MethodSignatureDescription
getAvatars(userIds: string[], size?: AvatarSize) => Promise<Record<string, string | null>>Fetch avatars with automatic batching and caching
getCachedAvatar(userId: string, size?: AvatarSize) => string | null | undefinedSynchronous cache read (undefined = not cached or expired)
clearCache() => voidClear all cached avatars, in memory and in sessionStorage
clearSingleCache(userId: string, size?: AvatarSize) => voidClear cache for one user (all sizes if omitted)
getCacheSize() => numberNumber 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.