Blog Implementation Guides 7 min read

Remix Image Loading and Broken URL Fallbacks

Implement image fallback patterns in Remix using loader functions, React onError handlers, and fallback.pics URLs for missing media in full-stack React routes.

remix image fallbackremix loaderreact image errorplaceholder imagefull-stack react
Remix Image Loading and Broken URL Fallbacks

Remix routes fetch data in loader functions on the server — that is the right place to resolve fallback image URLs before they reach the browser. Missing media fields in the loader response should never reach an img tag as undefined or null.

For images that can fail after the initial render — user-uploaded content or third-party CDN URLs — an onError handler on the img element provides the client-side safety net.

Loader pattern

Resolve fallback images in Remix loader functions

Remix loaders run on the server for every navigation. They are the cleanest place to resolve a missing image field because the fallback URL ends up in the serialized JSON response and is available before the component renders.

This approach also ensures that social crawlers, which parse server-rendered HTML, see the correct og:image meta tag rather than an empty or null value.

Implementation tsx
// app/routes/products.$slug.tsx
export async function loader({ params }: LoaderFunctionArgs) {
  const product = await db.product.findUnique({ where: { slug: params.slug } });
  if (!product) throw new Response('Not Found', { status: 404 });

  return json({
    ...product,
    image: product.image
      ?? `https://fallback.pics/api/v1/800x800/7C3AED/FFFFFF?text=${encodeURIComponent(product.name)}`,
  });
}

Client fallback

onError handler for Remix route components

Loader-resolved fallbacks cover missing fields in the database. But an image URL that exists in the database can still return a 404 if the original file was deleted from a CDN or S3 bucket. onError handles that case.

In React, set the handler via the onError prop (camelCase). Update a piece of state to swap the displayed src rather than mutating the DOM directly.

Implementation tsx
// app/components/ProductImage.tsx
export function ProductImage({ src, name }: { src: string; name: string }) {
  const [imgSrc, setImgSrc] = useState(src);
  const fallback = `https://fallback.pics/api/v1/800x800?text=${encodeURIComponent(name)}`;

  return (
    <img
      src={imgSrc}
      alt={name}
      width={800}
      height={800}
      onError={() => { if (imgSrc !== fallback) setImgSrc(fallback); }}
    />
  );
}

Meta function

Set og:image in the Remix meta export

Remix uses the meta export to set per-route meta tags. The loader data is available as an argument, so you can use the resolved image URL (including fallback) directly in the og:image tag.

Return og:image:width and og:image:height as separate meta entries. Facebook and LinkedIn crawlers use these values to decide whether to show the image or fall back to a no-image card.

Implementation tsx
// app/routes/blog.$slug.tsx
export function meta({ data }: MetaArgs) {
  return [
    { title: data.post.title },
    { property: 'og:image', content: data.post.image },
    { property: 'og:image:width', content: '1200' },
    { property: 'og:image:height', content: '630' },
  ];
}

Error boundary

When to use ErrorBoundary vs component onError

If an entire route crashes because an image fetch throws, an ErrorBoundary component can render a fallback UI rather than a full error page. This is uncommon for image-only failures but worth knowing.

For most image fallback cases, individual component state with onError is the right granularity. Use ErrorBoundary at the route level for data fetching failures, not for individual img load errors.

Avatar example

User avatar fallbacks in Remix apps

User profile images are one of the most common fallback cases in Remix apps. Resolve the avatar URL in the loader and use the avatar route with initials when the user has not uploaded a photo.

Initials-based avatars are more readable than a blank square and require no additional assets or client-side logic.

Implementation tsx
// In loader
const avatarUrl = user.avatar
  ?? `https://fallback.pics/api/v1/avatar/64?text=${getInitials(user.name)}`;

// Helper
function getInitials(name: string) {
  return name.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2);
}

Key takeaways

What to standardize before shipping

  • Resolve missing image fields in Remix loader functions so the fallback URL is in the page before client hydration.
  • Use React's onError prop (not HTML onerror) in Remix route components to handle CDN failures post-render.
  • Guard against infinite error loops: check whether imgSrc already equals the fallback before calling setImgSrc.
  • Use resolved loader data in the meta export to set og:image so social crawlers see the correct URL.
  • Avatar fallbacks with /api/v1/avatar/{size}?text= are more informative than generic placeholder squares.

Production fallback layer

Use fallback.pics anywhere an image URL is accepted.

Start with one deterministic URL and standardize fallback behavior across your design system.

Read the docs