How to Update React Suspense in React

React Suspense is a powerful feature that coordinates the loading states of your component tree, but updating suspended content without causing jarring UI layout shifts requires a specific approach. This article covers how to manage, trigger, and smoothly update React Suspense boundaries using modern React features like transitions, state updates, and cache invalidation.

Understanding How Suspense Updates Work

React Suspense does not have a direct “update” function. Instead, a Suspense boundary updates its visual state automatically based on the promises thrown by its child components.

When a child component requests new data (such as during a route change or search query), it “suspends” the render. React catches this, pauses rendering the children, and displays the fallback UI. Once the promise resolves with the updated data, React renders the child components with the new state.

To control this process smoothly and prevent the UI from flickering back to a loading spinner during updates, React provides specific hooks and patterns.

1. Preventing Fallbacks with useTransition

The most common issue when updating Suspense-enabled data is the sudden reappearance of the loading spinner. To update data while keeping the existing UI visible until the new data is ready, you should use the useTransition hook.

The useTransition hook marks a state update as a non-urgent transition. React will keep the old UI fully interactive while fetching the new data in the background.

import { useState, useTransition, Suspense } from 'react';

function SearchContainer() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleSearch(nextQuery) {
    // Wrap the state update in startTransition
    startTransition(() => {
      setQuery(nextQuery);
    });
  }

  return (
    <div>
      <SearchInput onChange={handleSearch} />
      <div style={{ opacity: isPending ? 0.8 : 1 }}>
        <Suspense fallback={<Spinner />}>
          <SearchResults query={query} />
        </Suspense>
      </div>
    </div>
  );
}

By wrapping setQuery in startTransition, React delays updating the Suspense boundary’s fallback. The user sees the old results (optionally styled with lower opacity to indicate a pending state) until the new search results are fully loaded.

2. Updating Suspense via Cache Invalidation

Because Suspense relies on a data-fetching cache, updating the data inside Suspense requires updating the cache container.

If you are using data-fetching libraries like TanStack Query (React Query) or SWR, you update Suspense by invalidating the query cache or triggering a refetch:

import { useQueryClient } from '@tanstack/react-query';

function UpdateButton() {
  const queryClient = useQueryClient();

  function handleUpdate() {
    // Invalidating the query triggers the Suspense boundary to fetch updated data
    queryClient.invalidateQueries({ queryKey: ['userData'] });
  }

  return <button onClick={handleUpdate}>Refresh Data</button>;
}

When the cache is invalidated, the query client throws a new promise, putting the Suspense boundary back into a loading or transition state while the updated data is fetched.

3. Forcing Updates with the key Prop

If you need to completely reset a Suspense boundary and force it to show its fallback spinner during an update, you can change the key prop on the Suspense component or its immediate child.

Changing the key forces React to unmount the old component tree, discard its state, and mount a brand new instance.

// Changing the userId key forces Suspense to show the fallback during the update
<Suspense key={userId} fallback={<ProfileSkeleton />}>
  <ProfileDetails id={userId} />
</Suspense>

Use this strategy when switching between entirely different logical entities (like switching from User A to User B) where showing old cached data would be confusing to the user.