Flexible Components in Catalyst with <Stream>

Blog graphic (4)

Create your store and start selling today.

Create your new website.

See if the BigCommerce platform is a good fit for your business.

No credit card required.

chris-nanninga-sm
Written by
Chris Nanninga

11/19/2025

Share this article

In modern React and Next.js applications, <Suspense> has become a key pattern for rendering fallback visual state while data is loading, enhancing the user experience by delivering fast-loading layout as quickly as possible while deferring slower loading sections of a web page. (If you’re unfamiliar with this React technique, check out our previous blog article, Understanding Suspense in Next.js!)

Catalyst, the Next.js storefront for BigCommerce, builds on the power of <Suspense> with its own <Stream> component and pattern for reusable components that handle dynamic data.  This architecture utilizes <Suspense> under the hood, applying a layer of abstraction to account for several common complexities.  The result is a powerful but easy-to-use fallback pattern that supports flexible, reusable, and agnostic components.

Design Goals

To recap the basic use of <Suspense>, the following typical scenario demonstrates the component <TopCategories> handling its own data loading (presumably a GraphQL fetch for root categories) while the page route uses <Suspense> to render a custom tailored fallback until <TopCategories> is ready to render.

page.tsx

top-categories.tsx

In Catalyst’s rich and flexible component library, two primary principles for versatile reusable components immediately upend the implementation seen above:

  • Components should handle their own fallbacks - The page route or parent component shouldn’t need to concern itself with whether a fallback is needed.  This is a concern of the component itself.

  • Components should be data agnostic - Components should be concerned only with presentation, not how the data is loaded. This makes for more reusable components that can support a variety of data loading strategies.

As the use of <Suspense> is adapted to support these principles, several more specific design goals present themselves:

  • Support for both synchronous and asynchronous data - Components shouldn’t be concerned with how data is fetched, seamlessly handling either loaded data for a Promise for that data.

  • Server-side and client-side compatibility - It should be possible to drop a component into a React server component or a client-side context, with any Promise being resolved appropriately in either context.

  • Deferred data fetching - Data fetching should be pushed as far “down” in the component tree as possible, for optimal support for Next.js Partial Prerendering.

  • Excellent developer experience - Both authoring and using components should feel natural and avoid getting bogged down with verbose conditional code.

<Stream> and its associated pattern answers these design goals and provides an important tool for building clean, data-aware components in your Catalyst implementation.

Starting with <Suspense>

If we visualize how we might directly use <Suspense> to accomplish our key principles - components that handle their own fallback but not their own data fetching - the following is a plausible starting point.

page.tsx

top-categories.tsx

Our developer experience is already starting to suffer, with the <TopCategories> component now split into separate components to make the wrapping in <Suspense> possible.

On top of this, several other design goals have yet to be met:

  • Sync vs. async data - The component explicitly accepts a Promise.

  • Context awareness - The way the Promise is being resolved will cause issues if this component is used in a client-side context.

In order to address these needs, more verbose typing and conditional code would need to be introduced, with the end result being even more unwieldy boilerplate to implement in every single component.

The <Stream> Pattern

With <Stream> providing an abstraction layer for these concerns, the following much cleaner implementation of <TopCategories> becomes possible:

top-categories.tsx

In this example, the main implementation of the component is simply wrapped in a callback function that is a child of <Stream>.

  • The component accepts a Streamable value, which may or may not be a Promise.

  • <Stream> accepts a fallback state (ultimately passed to <Suspense>), as well as the Streamable value.

  • Once the final data value is available (after a Promise is resolved if necessary), it’s passed to the callback function, rendering the JSX within it.  No more need for a separate component.

  • If the Streamable value is a Promise, <Stream> conditionally handles awaiting it appropriately in a server-side or client-side context.

From a developer experience standpoint, this is a simple pattern that can be consistently implemented in any component that handles dynamic data, allowing such a component to be maximally reusable.

Understanding Streamable.from 

A final part of the Catalyst pattern addresses another design goal that hasn’t yet been revisited:  pushing data loading further down in the component tree.

Observe the following tweak to the page route (or parent component):

page.tsx

Wrapping asynchronous data fetching in Streamable.from results in not just a Promise, but a “lazy” Promise that is not actually executed until it is awaited.  This is a good practice not only for data fetching, but for all operations that will trigger dynamic rendering (such as accessing request headers or cookies).  With this pattern, dynamic rendering doesn’t kick in here near the top of the page layout, but further down in the component tree where the data is used.

The final word

The <Stream> component pattern in Catalyst demonstrates how thoughtful abstractions can dramatically improve both developer experience and application performance. By abstracting away the complexity of handling <Suspense> boundaries, Promise resolution, and context awareness, <Stream> enables developers to build flexible, data-agnostic components that work seamlessly in both server and client contexts.

Resources

See our Demo Catalyst Streamable repo for examples comparing <Suspense> with the <Stream> pattern.

Build more than code. Build connections.

From edge cases to workarounds, learn from developers solving things in real time.