Skip to main content

React Suspense and try...catch

Free2019-12-07#JS#React Suspense Boundary#React Suspense原理#React Suspense机制#React Suspense教程#UI层try catch

Understand React Suspense in 5 lines of code

I. try...catch at UI Layer

First, let's state the conclusion: Suspense is like try...catch, determining whether UI is safe:

try {
  // Once there's something not ready
} catch {
  // Immediately enter catch block, go to fallback
}

So, how to define safety?

Think about it: if a component's code hasn't finished loading, and we try to render it, it's obviously unsafe. So, narrowly speaking, we can consider components whose code is ready as safe, including synchronous components and loaded asynchronous components (React.lazy), for example:

// Synchronous component, safe
import OtherComponent from './OtherComponent';
// Asynchronous component, unsafe
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
// ...After AnotherComponent code finishes loading
// Loaded asynchronous component, safe
AnotherComponent

Error Boundary

There's a similar thing called Error Boundary, also a form of try...catch at UI layer, where safety is defined as component code execution without JavaScript Error:

Error boundaries are React components that catch JavaScript errors anywhere in their child component tree, log those errors, and display a fallback UI instead of the component tree that crashed.

We find these two definitions don't conflict. In fact, Suspense and Error Boundary can indeed coexist, such as using Error Boundary to catch asynchronous component loading errors:

If the other module fails to load (for example, due to network failure), it will trigger an error. You can handle these errors to show a nice user experience and manage recovery with Error Boundaries.

For example:

import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

const MyComponent = () => (
  <div>
    <MyErrorBoundary>
      <Suspense fallback={<div>Loading...</div>}>
        <section>
          <OtherComponent />
          <AnotherComponent />
        </section>
      </Suspense>
    </MyErrorBoundary>
  </div>
);

II. Build a Suspense Yourself

The 5 lines of code at the beginning might be interesting, but not clear enough. Let's continue filling in:

function Suspense(props) {
  const { children, fallback } = props;
  try {
    // Once there's something not ready
    React.Children.forEach(children, function() {
      assertReady(this);
    });
  } catch {
    // Immediately enter catch block, go to fallback
    return fallback;
  }

  return children;
}

assertReady is an assertion that throws Error for unsafe components:

import { isLazy } from "react-is";

function assertReady(element) {
  // Lazy components that haven't finished loading are unsafe
  if (isLazy(element) && element.type._status !== 1) {
    throw new Error('Not ready yet.');
  }
}

P.S. react-is is used to distinguish Lazy components, and _status indicates the loading state of Lazy components. See React Suspense | Specific Implementation for details.

Let's try it out:

function App() {
  return (<>
    <Suspense fallback={<div>loading...</div>}>
      <p>Hello, there.</p>
    </Suspense>
    <Suspense fallback={<div>loading...</div>}>
      <LazyComponent />
    </Suspense>
    <Suspense fallback={<div>loading...</div>}>
      <ReadyLazyComponent />
    </Suspense>
    <Suspense fallback={<div>loading...</div>}>
      <p>Hello, there.</p>
      <LazyComponent />
      <ReadyLazyComponent />
    </Suspense>
  </>);
}

Corresponding UI content is:

Hello, there.
loading...
ready lazy component.
loading...

First render result meets expectations. As for the subsequent update process (replacing loading with actual content after component finishes loading), it belongs more to the category of Lazy component rendering mechanism, not much related to Suspense. Won't expand here. If interested, refer to React Suspense | Specific Implementation.

P.S. Among them, ReadyLazyComponent construction has a small trick:

const ReadyLazyComponent = React.lazy(() =>
  // Simulate import('path/to/SomeOtherComponent.js')
  Promise.resolve({
    default: () => {
      return <p>ready lazy component.</p>;
    }
  })
);

// Render Lazy Component once to trigger its loading, making it ready
const rootElement = document.getElementById("root");
// Only used to preload lazy component, ignore Warning about missing outer Suspense
ReactDOM.createRoot(rootElement).render(<ReadyLazyComponent />);

setTimeout(() => {
  // After above rendering finishes, ReadyLazyComponent is truly ready
});

Because Lazy Component only loads when truly needed to render (so-called lazy), render it once first, then it's ready when used again.

III. Analogy to try...catch

As described above, the correspondence between Suspense and try...catch is:

  • Suspense: corresponds to try

  • fallback: corresponds to catch

  • Lazy Component not yet loaded: corresponds to Error

Due to similarity in principle, many characteristics of Suspense can be easily understood by analogy to try...catch, for example:

  • Nearest fallback: After Error is thrown, look upward for the nearest try's corresponding catch

  • Fallback if there's unready component: In a large try block, as long as there's one Error, immediately enter catch

So, for a group of components wrapped by Suspense, either all are displayed (including possibly contained fallback content), or none are displayed (instead display that Suspense's fallback). Understanding this point is especially important for mastering Suspense.

Performance Impact

As in the example above:

<Suspense fallback={<div>loading...</div>}>
  <p>Hello, there.</p>
  <LazyComponent />
  <ReadyLazyComponent />
</Suspense>

Render result is loading..., because when processing LazyComponent, Suspense fallback is triggered. Whether already processed Hello, there., or not yet processed ReadyLazyComponent, neither can be displayed. So, there are 3 problems:

  • Collateral damage: One not-yet-loaded Lazy Component can prevent many components before it that could display immediately from displaying

  • Blocking rendering: Not-yet-loaded Lazy Component will block rendering flow, blocking rendering of all components after it under nearest Suspense ancestor, causing serial waiting

So, like using try...catch, abusing Suspense will also cause (UI layer) performance impact. Although technically wrapping the entire application in top-level Suspense can indeed provide fallback for all Lazy Components:

<Suspense fallback={<div>global loading...</div>}>
  <App />
</Suspense>

But must clearly realize the consequences of doing so.

Structural Characteristics

Suspense, like try...catch, eliminates conditional judgments by providing a fixed structure:

try {
  // If Error appears
} catch {
  // Then enter catch
}

Branch logic is solidified into syntax structure. Suspense is similar:

<Suspense fallback={ /* Then enter fallback */ }>
  { /* If unready Lazy component appears */ }
</Suspense>

The benefit of doing this is no need for conditional judgments in code, thus not depending on local state, we can easily adjust its scope:

<Suspense fallback={<div>loading...</div>}>
  <p>Hello, there.</p>
  <LazyComponent />
  <ReadyLazyComponent />
</Suspense>

Change to:

<p>Hello, there.</p>
<Suspense fallback={<div>loading...</div>}>
  <LazyComponent />
</Suspense>
<ReadyLazyComponent />

Almost no change cost before and after, even easier than adjusting try...catch boundaries (because no need to consider variable scope). This is very meaningful for harmlessly adjusting loading granularity and sequence:

Suspense lets us change the granularity of our loading states and orchestrate their sequencing without invasive changes to our code.

IV. Online Demo

All important examples involved in the article are in the Demo project (with detailed comments):

V. Summary

Suspense is like try...catch at UI layer, but what it catches is not exceptions, but components not yet loaded.

Of course, Error Boundary is also. The two catch their own things without conflict.

References

Comments

No comments yet. Be the first to share your thoughts.

Leave a comment