Skip to main content

Suspense

Suspend rendering while loading asynchronous data or code. Display a fallback UI until the suspended content is ready.

Suspense Component

Signature

interface SuspenseProps {
  children?: ComponentChildren;
  fallback?: ComponentChildren;
}

class Suspense extends Component<SuspenseProps> {}
children
ComponentChildren
The content to render once data is loaded.
fallback
ComponentChildren
The fallback UI to show while suspended (typically a loading indicator).

Usage

import { Suspense } from 'preact/compat';

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <UserProfile />
    </Suspense>
  );
}

lazy

Lazy load components for code splitting.

Signature

function lazy<T extends ComponentType<any>>(
  loader: () => Promise<{ default: T }>
): T
loader
() => Promise<{ default: T }>
required
A function that returns a Promise resolving to a module with a default export containing the component.
returns
T
A lazy-loaded component that can be rendered like a normal component.

Usage

import { Suspense, lazy } from 'preact/compat';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  return (
    <Suspense fallback={<div>Loading component...</div>}>
      <HeavyComponent />
    </Suspense>
  );
}

Complete Examples

Lazy Loading Routes

import { Suspense, lazy } from 'preact/compat';
import { Router, Route } from 'preact-router';

const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const Dashboard = lazy(() => import('./routes/Dashboard'));

function App() {
  return (
    <Suspense fallback={<div className="loader">Loading...</div>}>
      <Router>
        <Route path="/" component={Home} />
        <Route path="/about" component={About} />
        <Route path="/dashboard" component={Dashboard} />
      </Router>
    </Suspense>
  );
}

Multiple Lazy Components

import { Suspense, lazy } from 'preact/compat';

const Sidebar = lazy(() => import('./Sidebar'));
const Content = lazy(() => import('./Content'));

function Dashboard() {
  return (
    <div className="dashboard">
      <Suspense fallback={<div>Loading sidebar...</div>}>
        <Sidebar />
      </Suspense>
      
      <Suspense fallback={<div>Loading content...</div>}>
        <Content />
      </Suspense>
    </div>
  );
}

Nested Suspense

import { Suspense, lazy } from 'preact/compat';

const Comments = lazy(() => import('./Comments'));
const UserProfile = lazy(() => import('./UserProfile'));

function Post() {
  return (
    <Suspense fallback={<PageLoader />}>
      <article>
        <h1>Blog Post</h1>
        
        <Suspense fallback={<ProfileSkeleton />}>
          <UserProfile />
        </Suspense>
        
        <p>Post content...</p>
        
        <Suspense fallback={<CommentsSkeleton />}>
          <Comments />
        </Suspense>
      </article>
    </Suspense>
  );
}

Data Fetching with Suspense

import { Suspense } from 'preact/compat';

// Create a resource that suspends
function fetchUser(userId) {
  let status = 'pending';
  let result;
  
  const promise = fetch(`/api/users/${userId}`)
    .then(res => res.json())
    .then(data => {
      status = 'success';
      result = data;
    })
    .catch(error => {
      status = 'error';
      result = error;
    });
  
  return {
    read() {
      if (status === 'pending') throw promise;
      if (status === 'error') throw result;
      return result;
    }
  };
}

const userResource = fetchUser(1);

function UserProfile() {
  const user = userResource.read();
  
  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.email}</p>
    </div>
  );
}

function App() {
  return (
    <Suspense fallback={<div>Loading user...</div>}>
      <UserProfile />
    </Suspense>
  );
}

Implementation Details

Suspense Component

The Suspense implementation:
export function Suspense() {
  this._pendingSuspensionCount = 0;
  this._suspenders = null;
  this._detachOnNextRender = null;
}

Suspense.prototype = new Component();

Suspense.prototype.render = function (props, state) {
  if (this._detachOnNextRender) {
    // Handle detaching suspended content
    if (this._vnode._children) {
      const detachedParent = document.createElement('div');
      const detachedComponent = this._vnode._children[0]._component;
      this._vnode._children[0] = detachedClone(
        this._detachOnNextRender,
        detachedParent,
        (detachedComponent._originalParentDom = detachedComponent._parentDom)
      );
    }
    this._detachOnNextRender = null;
  }

  return [
    createElement(Fragment, null, state._suspended ? null : props.children),
    state._suspended && createElement(Fragment, null, props.fallback)
  ];
};

lazy Function

The lazy implementation:
export function lazy(loader) {
  let prom;
  let component = null;
  let error;
  let resolved;

  function Lazy(props) {
    if (!prom) {
      prom = loader();
      prom.then(
        exports => {
          if (exports) {
            component = exports.default || exports;
          }
          resolved = true;
        },
        e => {
          error = e;
          resolved = true;
        }
      );
    }

    if (error) {
      throw error;
    }

    if (!resolved) {
      throw prom;
    }

    return component ? createElement(component, props) : null;
  }

  Lazy.displayName = 'Lazy';
  return Lazy;
}

How Suspense Works

  1. Promise Thrown: A component throws a Promise when data isn’t ready
  2. Suspense Catches: Nearest Suspense boundary catches the Promise
  3. Fallback Shown: Suspense renders the fallback prop
  4. Promise Resolves: When the Promise resolves, the component re-renders
  5. Content Displayed: Real content replaces the fallback

Error Boundaries with Suspense

Combine with error boundaries for error handling:
import { Component, Suspense, lazy } from 'preact/compat';

class ErrorBoundary extends Component {
  state = { hasError: false };
  
  static getDerivedStateFromError(error) {
    return { hasError: true };
  }
  
  componentDidCatch(error, errorInfo) {
    console.error('Error:', error, errorInfo);
  }
  
  render() {
    if (this.state.hasError) {
      return <div>Something went wrong.</div>;
    }
    return this.props.children;
  }
}

const LazyComponent = lazy(() => import('./Component'));

function App() {
  return (
    <ErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <LazyComponent />
      </Suspense>
    </ErrorBoundary>
  );
}

Fallback Components

Simple Loader

function Loader() {
  return (
    <div className="loader">
      <div className="spinner" />
      <p>Loading...</p>
    </div>
  );
}

<Suspense fallback={<Loader />}>
  <Content />
</Suspense>

Skeleton Screen

function UserSkeleton() {
  return (
    <div className="skeleton">
      <div className="skeleton-avatar" />
      <div className="skeleton-line" />
      <div className="skeleton-line short" />
    </div>
  );
}

<Suspense fallback={<UserSkeleton />}>
  <UserProfile />
</Suspense>

Best Practices

  1. Granular Suspense: Use multiple Suspense boundaries for better UX
  2. Meaningful Fallbacks: Show skeleton screens instead of generic spinners
  3. Error Boundaries: Always wrap Suspense in error boundaries
  4. Loading States: Consider showing partial content while loading
  5. Code Splitting: Use lazy for route-based code splitting

Transition to Loaded Content

Use CSS for smooth transitions:
.content {
  animation: fadeIn 0.3s ease-in;
}

@keyframes fadeIn {
  from { opacity: 0; }
  to { opacity: 1; }
}
<Suspense fallback={<Loader />}>
  <div className="content">
    <UserProfile />
  </div>
</Suspense>

useTransition Hook

For smoother transitions between loading states:
import { useState, useTransition, Suspense, lazy } from 'preact/compat';

const Tab1 = lazy(() => import('./Tab1'));
const Tab2 = lazy(() => import('./Tab2'));

function Tabs() {
  const [tab, setTab] = useState('tab1');
  const [isPending, startTransition] = useTransition();
  
  const switchTab = (newTab) => {
    startTransition(() => {
      setTab(newTab);
    });
  };
  
  return (
    <>
      <button onClick={() => switchTab('tab1')}>Tab 1</button>
      <button onClick={() => switchTab('tab2')}>Tab 2</button>
      
      {isPending && <div>Loading tab...</div>}
      
      <Suspense fallback={<div>Loading...</div>}>
        {tab === 'tab1' ? <Tab1 /> : <Tab2 />}
      </Suspense>
    </>
  );
}

Source

Implementation: compat/src/suspense.js:1-255

Build docs developers (and LLMs) love