Table of Contents
Brief background
The exact function of Suspense
Example: Navigation
Navigation Assistant Program
Suspense-based navigation components
Put everything together
Search and update
Quick Note on Consistency
Nested Suspense boundaries
How to integrate data
Wait, what about preloading? !
But seriously, how did you preload?
Summarize
Home Web Front-end CSS Tutorial React Suspense in Practice

React Suspense in Practice

Apr 08, 2025 am 10:42 AM

React Suspense in Practice

This article explores the working mechanism, functionality of React Suspense and how to integrate in real web applications. We will learn how to integrate routing and data loading with Suspense in React. For routing, I will use native JavaScript and use my own micro-graphql-react GraphQL library for data processing.

If you are considering using React Router it looks great, but I never had a chance to use it. My own personal project has a simple enough routing solution that I have always done manually. Additionally, using native JavaScript will give us a better understanding of how Suspense works.

Brief background

Let's talk about Suspense itself. Kingsley Silas gives a comprehensive overview of it, but the first thing to note is that it is still an experimental API. This means—and React's documentation says so—don't use it for work in a production environment. It can change at any time until it is fully completed, so keep this in mind.

That is, Suspense's core lies in maintaining a consistent UI when asynchronous dependencies (such as lazy loading React components, GraphQL data, etc.). Suspense provides low-level APIs that allow you to easily maintain the UI when your application manages this content.

But in this case, what does "consistent" mean? This means that the partially completed UI is not rendered. This means that if there are three data sources on the page, one of which is completed, we do not want to render the updated state fragment, and the load indicators for the other two outdated state fragments next to it.

What we want to do is indicate to the user that the data is loading, while continuing to display the old UI, or displaying an alternative UI to indicate that we are waiting for the data; Suspense supports both, which I will go into more detail later.

The exact function of Suspense

This is much simpler than it seems. Traditionally, in React, you set the status and your UI will be updated. Life is simple. But it also leads to inconsistencies in the types mentioned above. Suspense adds a feature that enables the component to notify React when rendering it is waiting for asynchronous data; this is called a hang, which can happen anywhere in the component tree and can happen multiple times as needed until the tree is ready. When a component hangs, React refuses to render the suspended state update until all pending dependencies are satisfied.

So, what happens when the component is suspended? React will look up the tree and find the first one<suspense></suspense> component and render its fallback content. I'll provide a lot of examples, but for now, please know that you can provide the following:

<suspense fallback="{<Loading"></suspense> }>
Copy after login

……if<suspense></suspense> Any child components of the hang will be rendered<loading></loading> Components.

But what happens if we already have a valid, consistent UI and the user loads new data, causing the component to hang? This will cause the entire existing UI to unrender and display the fallback content. This is still consistent, but it is not a good user experience. We would rather the old UI stay on the screen when new data is loaded.

To support this, React provides a second API, useTransition , which effectively makes state changes happen in memory . In other words, it allows you to set state in memory while keeping the existing UI on the screen; React literally retains a second copy of the component tree in memory and sets state on that tree. The component may hang, but only hangs in memory, so your existing UI will continue to appear on the screen. When the state change is complete and all pending is resolved, the state change in memory is rendered to the screen. Obviously you want to give feedback to the user in the process, so useTransition provides a pending boolean value that you can use to display some kind of inline "load" notification while resolving the pending in memory.

When you consider it, you may not want to display an existing UI indefinitely when the load is suspended. If the user tries to perform something and a long time has passed before it is finished, you should probably consider that the existing UI is outdated and invalid. At this point, you may indeed want your component tree to hang and display<suspense></suspense> Rewind content.

To achieve this, useTransition takes the timeoutMs value. This means you are willing to let the in-memory state change run for the amount of time before hanging.

 const Component = props => {
  const [startTransition, isPending] = useTransition({ timeoutMs: 3000 });
  // .....
};
Copy after login

Here, startTransition is a function. When you want to run a state change "in memory", you can call startTransition and pass a lambda expression that performs the state change.

 startTransition(() => {
  dispatch({ type: LOAD_DATA_OR_SOMETHING, value: 42 });
});
Copy after login

You can call startTransition anywhere. You can pass it to child components, etc. When you call it, any state changes you perform will occur in memory. If a pending occurs, isPending will become true, which you can use to display some kind of inline load indicator.

That's it. This is what Suspense does.

The rest of this article will introduce some actual code to take advantage of these features.

Example: Navigation

To associate navigation with Suspense, you'll be happy to know that React provides a primitive to do this: React.lazy . It is a function that accepts a lambda expression that returns a Promise, which resolves to a React component. The result of this function call will become your lazy loading component. It sounds complicated, but it looks like this:

 const SettingsComponent = lazy(() => import("./modules/settings/settings"));
Copy after login

SettingsComponent is now a React component that when rendered (but not before), will call the function we passed in which will call import() and load the JavaScript module located in ./modules/settings/settings .

The key part is: while import() is in progress, the component that renders SettingsComponent will hang. It looks like we have all the components in hand, so let's put them together and build some Suspense-based navigation.

But first, to provide context, I'll briefly introduce how navigation state is managed in this application so that Suspense code makes more sense.

I will use my booklist application. It's just a personal project of myself, which I mainly use to experiment with cutting-edge web technologies. It was written by me alone, so expect some of it to be a little rough (especially the design).

The app is small and users can browse about eight different modules without any deeper navigation. Any search status that the module may use is stored in the query string of the URL. With this in mind, there are ways to extract the current module name and search status from the URL. This code uses npm's query-string and history packages, which looks a bit like this (some details were removed for simplicity, such as authentication).

 import createHistory from "history/createBrowserHistory";
import queryString from "query-string";
export const history = createHistory();
export function getCurrentUrlState() {
  let location = history.location;
  let parsed = queryString.parse(location.search);
  return {
    pathname: location.pathname,
    searchState: parsed
  };
}
export function getCurrentModuleFromUrl() {
  let location = history.location;
  return location.pathname.replace(/\//g, "").toLowerCase();
}
Copy after login

I have an appSettings reducer that holds the current module and searchState values ​​of the application and uses these methods to sync with the URL when needed.

Suspense-based navigation components

Let's start with some Suspense work. First, let's create lazy loaded components for our module.

 const ActivateComponent = lazy(() => import("./modules/activate/activate"));
const AuthenticateComponent = lazy(() => import("./modules/authenticate/authenticate"));
const BooksComponent = lazy(() => import("./modules/books/books"));
const HomeComponent = lazy(() => import("./modules/home/home"));
const ScanComponent = lazy(() => import("./modules/scan/scan"));
const SubjectsComponent = lazy(() => import("./modules/subjects/subjects"));
const SettingsComponent = lazy(() => import("./modules/settings/settings"));
const AdminComponent = lazy(() => import("./modules/admin/admin"));
Copy after login

Now we need a way to select the correct component based on the current module. If we use React Router, we will have some nice ones<route></route> Components. Since we are doing this manually, switch statement will work.

 export const getModuleComponent = moduleToLoad => {
  if (moduleToLoad == null) {
    return null;
  }
  switch (moduleToLoad.toLowerCase()) {
    case "activate":
      return ActivateComponent;
    case "authenticate":
      return AuthenticateComponent;
    case "books":
      return BooksComponent;
    case "home":
      return HomeComponent;
    case "scan":
      return ScanComponent;
    case "subjects":
      return SubjectsComponent;
    case "settings":
      return SettingsComponent;
    case "admin":
      return AdminComponent;
  }

  return HomeComponent;
};
Copy after login

Put everything together

After all the boring setups are done, let's see what the entire application root looks like. There is a lot of code here, but I promise that relatively few of these lines are related to Suspense and I'll cover all of them.

 const App = () => {
  const [startTransitionNewModule, isNewModulePending] = useTransition({
    timeoutMs: 3000
  });
  const [startTransitionModuleUpdate, moduleUpdatePending] = useTransition({
    timeoutMs: 3000
  });
  let appStatePacket = useAppState();
  let [appState, _, dispatch] = appStatePacket;
  let Component = getModuleComponent(appState.module);
  useEffect(() => {
    startTransitionNewModule(() => {
      dispatch({ type: URL_SYNC });
    });
  }, []);
  useEffect(() => {
    return history.listen(location => {
      if (appState.module != getCurrentModuleFromUrl()) {
        startTransitionNewModule(() => {
          dispatch({ type: URL_SYNC });
        });
      } else {
        startTransitionModuleUpdate(() => {
          dispatch({ type: URL_SYNC });
        });
      }
    });
  }, [appState.module]);
  Return (
    <appcontext.provider value="{appStatePacket}">
      <moduleupdatecontext.provider value="{moduleUpdatePending}">
        <div>
          <mainnavigationbar></mainnavigationbar>
          {isNewModulePending?<loading></loading> : null}
          <suspense fallback="{<LongLoading"></suspense> }>
            <div style="{{" flex: overflowy:>
              {Component?<component updating="{moduleUpdatePending}"></component> : null}
            </div>
          
        </div>
      </moduleupdatecontext.provider>
    </appcontext.provider>
  );
};
Copy after login

First, we made two different calls to useTransition . We use one for routing to the new module and the other for updating the search status of the current module. Why is there a difference? Well, the module may want to display an inline load indicator when the module's search status is being updated. The update status is saved by the moduleUpdatePending variable, you will see that I put it in the context so that the active module can be fetched and used as needed:

<div>
  <mainnavigationbar></mainnavigationbar>
  {isNewModulePending?<loading></loading> : null}
  <suspense fallback="{<LongLoading"></suspense> }>
    <div style="{{" flex: overflowy:>
      {Component?<component updating="{moduleUpdatePending}"></component> : null}
    </div>
  
</div>
Copy after login

appStatePacket is the result of the (but not shown) application state reducer discussed above. It contains various application state fragments that are rarely changed (color theme, offline state, current module, etc.).

 let appStatePacket = useAppState();
Copy after login

Later, I will get any active component based on the current module name. Initially this will be null.

 let Component = getModuleComponent(appState.module);
Copy after login

The first call to useEffect will tell us that appSettings reducer is synchronized with the URL at startup.

 useEffect(() => {
  startTransitionNewModule(() => {
    dispatch({ type: URL_SYNC });
  });
}, []);
Copy after login

Since this is the initial module that the web application navigates to, I wrap it in startTransitionNewModule to indicate that the new module is being loaded. While it might be tempting to set the initial module name of the reducer as its initial state, this prevents us from calling startTransitionNewModule callback, which means that our Suspense boundary will render the fallback content immediately, not after the timeout.

A history subscription is set to the next call to useEffect . Anyway, when the url changes, we will tell our application settings to sync with the URL. The only difference is which startTransition the same call is wrapped.

 useEffect(() => {
  return history.listen(location => {
    if (appState.module != getCurrentModuleFromUrl()) {
      startTransitionNewModule(() => {
        dispatch({ type: URL_SYNC });
      });
    } else {
      startTransitionModuleUpdate(() => {
        dispatch({ type: URL_SYNC });
      });
    }
  });
}, [appState.module]);
Copy after login

If we are browsing to a new module, we will call startTransitionNewModule . If we are loading a component that has not been loaded, React.lazy will hang, and only the suspend indicator visible to the root of the application will be set, which will show the load spinner at the top of the application when the lazy loaded component is fetched and loaded. Due to how useTransition works, the current screen will continue to display for three seconds. If that time is exceeded and the component is still not ready, our UI will hang and the fallback content will be rendered, which will display<longloading></longloading> Components:

 {isNewModulePending?<loading></loading> : null}
<suspense fallback="{<LongLoading"></suspense> }>
  <div style="{{" flex: overflowy:>
    {Component?<component updating="{moduleUpdatePending}"></component> : null}
  </div>
Copy after login

If we don't change the module, we will call startTransitionModuleUpdate :

 startTransitionModuleUpdate(() => {
  dispatch({ type: URL_SYNC });
});
Copy after login

If the update causes a hang, the hang indicator we will put in the context will be fired. The active component can detect this and display any inline load indicator it wants. As before, if the hang time exceeds three seconds, the same Suspense boundary will be triggered...unless, as we will see later, the Suspense boundary exists in the lower part of the tree.

One important thing to note is that these three-second timeouts are not only for component loading, but also for ready display. If the component is loaded in two seconds and is suspended when rendering in memory (because we are inside startTransition call), useTransition will continue to wait for up to one second before hanging again.

While writing this post, I used Chrome's slow network mode to help force loading to slow down to test my Suspense boundaries. Settings are located in the Network tab of Chrome Development Tools.

Let's open our application to the Settings module. This will be called:

 dispatch({ type: URL_SYNC });
Copy after login
Copy after login

Our appSettings reducer will sync with the URL and then set the module to "settings". This will happen inside startTransitionNewModule so that it will hang when the lazy loaded component tries to render. Since we are inside startTransitionNewModule , isNewModulePending will switch to true and will render<loading></loading> Components.

So, what happens when we browse to a new place? Basically the same as before, except for this call:

 dispatch({ type: URL_SYNC });
Copy after login
Copy after login

...will be from the second instance of useEffect . Let's browse to the book module and see what happens. First, the inline spinner shows as expected:

(Screenshots should be inserted here)

Search and update

Let's stay in the book module and update the URL query string to start a new search. Recall that we detected the same module in the second useEffect call and used a dedicated useTransition call for this. From there, we put the suspend indicator in the context so that any active module that can be retrieved and used for us.

Let's take a look at some code that actually uses it. There is not much Suspense-related code here. I get the value from the context and if true, render an inline spinner on top of my existing results. Recall this happens when the useTransition call has started and the application is suspended in memory . During this time we will continue to display the existing UI, but with this load indicator.

 const BookResults = ({ books, uiView }) => {
  const isUpdating = useContext(ModuleUpdateContext);
  Return (
    <div>
      {!books.length ? (
        <div classname="alert alert-warning" style="{{" margintop: marginright:>
          No books found
        </div>
      ) : null}
      {isUpdating?<loading></loading> : null}
      {uiView.isGridView ? (
        <gridview books="{books}"></gridview>
      ) : uiView.isBasicList ? (
        <basiclistview books="{books}"></basiclistview>
      ) : uiView.isCoversList ? (
        <coversview books="{books}"></coversview>
      ) : null}
    </div>
  );
};
Copy after login

Let's set up a search term and see what happens. First, the inline spinner displays.

Then, if useTransition timed out, we will get the fallback content of the Suspense boundary. The book module defines its own Suspense boundary to provide a finer loading indicator, as shown below:

(Screenshots should be inserted here)

This is a key point. Try not to display any spinner and "Loading" messages when creating Suspense boundary fallbacks. This makes sense for our top navigation because there is nothing else to do. However, when you are in a specific part of the application, try to make the fallback content reuse many of the same components and display some kind of load indicator where the data is - but everything else is disabled.

Here is what the relevant components of my book module look like:

 const RenderModule = () => {
  const uiView = useBookSearchUiView();
  const [lastBookResults, setLastBookResults] = useState({
    totalPages: 0,
    resultsCount: 0
  });
  Return (
    <div classname="standard-module-container margin-bottom-lg">
      <suspense fallback="{<Fallback" uiview="{uiView}"></suspense> }>
        <maincontent setlastbookresults="{setLastBookResults}" uiview="{uiView}"></maincontent>
      
    </div>
  );
};
const Fallback = ({ uiView, totalPages, resultsCount }) => {
  Return (
    <div>
      <booksmenubardisabled resultscount="{resultsCount}" totalpages="{totalPages}"></booksmenubardisabled>
      {uiView.isGridView ? (
        <gridviewshell></gridviewshell>
      ) : (
        <h1>
          Books are loading<i classname="fas fa-cog fa-spin"></i>
        </h1>
      )}
    </div>
  );
};
Copy after login

Quick Note on Consistency

Before we proceed, I want to point out one thing in the previous screenshot. Check out the inline spinner that appears when the search is suspended, then check out the screen when the search is suspended, and then see the completed results:

(Screenshots should be inserted here)

Note if there is a "C" tag on the right side of the search pane, and is there an option to remove it from the search query? Or, note that the tag is only on the last two screenshots? The moment the URL is updated, the application status that controls the tag is indeed updated; however, the status will not be displayed initially. Initially, the status update hangs in memory (because we used useTransition ) and the previous UI continues to display.

Then the content is backed up. The fallback content renders a disabled version of the same search bar, which does show the current search status (based on the selection). We have now removed the previous UI (because now it is quite old and stale) and are waiting to disable the search displayed in the menu bar.

This is the type of consistency Suspense provides you with free.

You can spend time making beautiful application states, and React does the job of inferring whether things are ready without you having to deal with the Promise.

Nested Suspense boundaries

Suppose our top navigation takes a while to load our book components into the extent to which our "still loading, sorry" spinner renders from the Suspense boundary. From there, the book component is loaded, and the new Suspense boundary within the book component is rendered. But then, as the rendering continues, our book search query fires and hangs. what happens? Will the top Suspense boundary continue to display until everything is ready, or will the lower-level Suspense boundary in the book take over?

The answer is the latter. When new Suspense boundaries are rendered at a lower position in the tree, their fallback content replaces the fallback content of any previous Suspense back content that has been displayed. There is currently an unstable API that can override this feature, but if you are making fallback content well, this may be the behavior you want. You do not want "still loading, sorry" to continue to display. Instead, once the book component is ready, you definitely want to display a shell with a more targeted waiting message.

Now what if our book module loads and starts rendering when startTransition spinner is still showing and then hangs? In other words, suppose our startTransition timeout is three seconds, the book component renders, the nested Suspense boundary is in the component tree after one second, and the search query is suspended. Will the remaining two seconds pass or will the fallback be displayed immediately before that new nested Suspense boundary renders the fallback content? The answer may be surprising, by default, the new Suspense fallback content will be displayed immediately. This is because it is best to display a new valid UI as soon as possible so that the user can see things are happening and are moving.

How to integrate data

Navigation is good, but how does data loading fit into it all?

It blends into it completely transparently. Data loading triggers a hang as it does with React.lazy 's navigation, and it hooks with all the same useTransition and Suspense boundaries. That's why Suspense is so amazing: all asynchronous dependencies work seamlessly in this same system. Prior to Suspense, manually managing these various asynchronous requests to ensure consistency was a nightmare, which is exactly why no one did it. Web applications are notorious for cascading spinners that stop at unpredictable times, producing inconsistent UIs that are only partially completed.

OK, but how do we actually associate the data load with it? Loading data in Suspense is both more complex and simple.

Let me explain.

If you are waiting for data, you will throw a promise in the component that reads (or tries to read) the data. Promise should be consistent based on data requests. Therefore, four duplicate requests for the same "C" search query should throw the same and same promise. This means some sort of cache layer to manage all of this. You probably won't write this yourself. Instead, you will only want and wait for the database you use to update itself to support Suspense.

This work has been done in my micro-graphql-react library. You will use the useSuspenseQuery hook instead of the useQuery hook, which has the same API, but will throw a consistent promise when you wait for the data.

Wait, what about preloading? !

Read other things about Suspense, talking about waterfalls, rendering, preloading, etc., is your brain already getting confused? don’t worry. That's what it all means.

Suppose you delay loading the book component, which then requests some data, which will result in a new Suspense. The network requests of components and network requests of data will occur one by one—in a waterfall manner.

But the key part is: when you start loading the component, the application state that causes any initial query to run is already available (in this case the URL). So why not "start" the query immediately when you know you need it? Once you browse to /books, why not start the current search query right away at that time so that it is already in progress when the component is loading?

The micro-graphql-react module does have a preload method, and I recommend you to use it. Preloading data is a nice performance optimization, but it has nothing to do with Suspense. Classic React applications can (and should) preload data immediately when they know they need it. Vue applications should preload data immediately when they know they need it. The Svelte application should...you get it.

The preloaded data is orthogonal to Suspense, which is what you can do with any framework. This is also something we all should have done, even though others have not done it.

But seriously, how did you preload?

It's up to you. At the very least, the logic to run the current search absolutely needs to be completely separated into its own independent module. You should literally make sure this preload function is in a separate file. Don't rely on webpack for tree-like jitter; you may face complete sadness the next time you review your package.

You have a preload() method in its own package, so call it. Call it when you know you want to navigate to the module. I'm assuming React Router has some APIs that can run code when navigation changes. For the native routing code above, I called the method in the previous route switching. I omitted it for brevity, but the book entry actually looks like this:

 switch (moduleToLoad.toLowerCase()) {
  case "activate":
    return ActivateComponent;
  case "authenticate":
    return AuthenticateComponent;
  case "books":
    // Preload! ! !
    booksPreload();
    return BooksComponent;
Copy after login

That's it. Here is a live demonstration that you can try:

(The link should be inserted here)

To modify the Suspense timeout value (default is 3000 milliseconds), navigate to Settings and view the other tabs. After modification, be sure to refresh the page.

Summarize

I've been so excited about anything in the web development ecosystem as I do with Suspense. It is an extremely ambitious system for managing one of the toughest problems in web development: asynchronicity.

The above is the detailed content of React Suspense in Practice. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

Java Tutorial
1655
14
PHP Tutorial
1254
29
C# Tutorial
1228
24
Google Fonts   Variable Fonts Google Fonts Variable Fonts Apr 09, 2025 am 10:42 AM

I see Google Fonts rolled out a new design (Tweet). Compared to the last big redesign, this feels much more iterative. I can barely tell the difference

How to Create an Animated Countdown Timer With HTML, CSS and JavaScript How to Create an Animated Countdown Timer With HTML, CSS and JavaScript Apr 11, 2025 am 11:29 AM

Have you ever needed a countdown timer on a project? For something like that, it might be natural to reach for a plugin, but it’s actually a lot more

HTML Data Attributes Guide HTML Data Attributes Guide Apr 11, 2025 am 11:50 AM

Everything you ever wanted to know about data attributes in HTML, CSS, and JavaScript.

How to select a child element with the first class name item through CSS? How to select a child element with the first class name item through CSS? Apr 05, 2025 pm 11:24 PM

When the number of elements is not fixed, how to select the first child element of the specified class name through CSS. When processing HTML structure, you often encounter different elements...

Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Why are the purple slashed areas in the Flex layout mistakenly considered 'overflow space'? Apr 05, 2025 pm 05:51 PM

Questions about purple slash areas in Flex layouts When using Flex layouts, you may encounter some confusing phenomena, such as in the developer tools (d...

A Proof of Concept for Making Sass Faster A Proof of Concept for Making Sass Faster Apr 16, 2025 am 10:38 AM

At the start of a new project, Sass compilation happens in the blink of an eye. This feels great, especially when it’s paired with Browsersync, which reloads

How We Created a Static Site That Generates Tartan Patterns in SVG How We Created a Static Site That Generates Tartan Patterns in SVG Apr 09, 2025 am 11:29 AM

Tartan is a patterned cloth that’s typically associated with Scotland, particularly their fashionable kilts. On tartanify.com, we gathered over 5,000 tartan

See all articles