Cascading Providers in Next.js: A Safer Alternative to Middleware for Sitecore and PWA Apps

Use Cascading Providers to manage global logic (e.g., profile loading, error handling) instead of modifying Next.js middleware, especially in apps using Sitecore or service workers.

July 31, 2025

By Sohrab Saboori

Next.js Cascading Providers: Middleware-Free State Management for Sitecore & PWAs

When building complex PWAs with Sitecore and Next.js, developers often reach for middleware to manage global state or load data. However, in highly integrated environments, modifying middleware can introduce risks, regressions, and maintenance headaches.

This article introduces the Cascading Providers Pattern — a modular, testable, and Sitecore-safe solution for managing global logic without middleware changes.

Not using Sitecore?

The Cascading Providers Pattern is still a great way to modularize global logic in large-scale React or Next.js apps — whether you’re building a SaaS dashboard, e-commerce site, or internal tool.

Why Avoid Middleware Modifications?

1. Sitecore Integration Complexity

Sitecore comes with its own middleware stack that handles:

  • Authentication flows
  • Content delivery
  • Experience optimization
  • Personalization rules

Modifying this can break critical functionality or create conflicts during Sitecore updates.

In Sitecore XM Cloud, the Next.js middleware often integrates with the Sitecore Experience Editor, layout resolution, or identity providers. Modifying it can break personalization, inline editing, or preview modes, especially in production or editing environments.

2. PWA Service **Worker Conflicts

PWAs rely heavily on service workers for:

  • Offline functionality
  • Caching strategies
  • Background sync
  • Push notifications

Middleware changes can interfere with these critical PWA features.

3. Deployment and Maintenance

  • Easier debugging: Issues are isolated to specific providers
  • Safer deployments: No risk of breaking core Next.js functionality
  • Better testing: Each provider can be tested independently
  • Team collaboration: Frontend teams can work without touching backend middleware

The Cascading Providers Pattern

What is it?

The Cascading Providers Pattern involves creating a hierarchy of React Context Providers that each handle specific concerns, cascading from general to specific functionality.

// Traditional approach - everything in one place
<App>
  <Component /> // Has to handle its own data loading
</App>
// Cascading Providers approach
<PWAProvider>
  <GlobalProfileProvider>
    <AuthenticationProvider>
      <App>
        <Component /> // Data is automatically available
      </App>
    </AuthenticationProvider>
  </GlobalProfileProvider>
</PWAProvider>

Implementation Example

Here's how we implemented this pattern in our PWA:

1. Base PWA Context Provider

// contexts/PWAContext.tsx
const PWAProvider = ({ children }) => {
  const [profileEmail, setProfileEmail] = useState("");
  const [currentAccount, setCurrentAccount] = useState(null);
  const [isOffline, setIsOffline] = useState(false);
  // PWA-specific functionality
  const value = {
    profileEmail,
    setProfileEmail,
    currentAccount,
    setCurrentAccount,
    isOffline,
    // ... other PWA state
  };
  return <PWAContext.Provider value={value}>{children}</PWAContext.Provider>;
};

2. Global Profile Provider (Middleware Alternative)

This provider acts like middleware by automatically loading user profile data after authentication.

We use the following custom hooks in this provider:

  • usePWAContext(): Accesses and updates PWA-specific global state (e.g., email, offline status)
  • useProfileDetails(): Custom hook that fetches detailed profile data from our backend
  • useSession(): Comes from next-auth to manage authentication status
// providers/GlobalProfileProvider.tsx
export const GlobalProfileProvider = ({ children }) => {
  const { profileEmail, showError } = usePWAContext();
  const { triggerProfileDetails, isProfileDetailsLoading } =
    useProfileDetails();
  const { data: session, status } = useSession();
  const hasTriggeredRef = useRef(false);
  const loadProfile = useCallback(async () => {
    try {
      await triggerProfileDetails();
    } catch (error) {
      showError({
        title: "Profile Load Error",
        message:
          "Failed to load profile details. Please try refreshing the page.",
        retryAction: () => loadProfile(),
      });
    }
  }, [triggerProfileDetails, showError]);
  useEffect(() => {
    // Only trigger if user is authenticated and we don't have profile data
    if (
      status === "authenticated" &&
      session?.user &&
      !profileEmail &&
      !isProfileDetailsLoading &&
      !hasTriggeredRef.current
    ) {
      console.log("GlobalProfileProvider: Loading profile data...");
      hasTriggeredRef.current = true;
      loadProfile();
    }
  }, [status, session, profileEmail, isProfileDetailsLoading, loadProfile]);
  return <>{children}</>;
};

3. App Integration

// pages/_app.tsx
function App({ Component, pageProps }) {
  return (
    <PWAProvider>
      <GlobalProfileProvider>
        <SessionProvider session={pageProps.session}>
          <I18nProvider lngDict={dictionary} locale={pageProps.locale}>
            <Component {...pageProps} />
            <GlobalErrorModal />
            <OfflineHandler />
          </I18nProvider>
        </SessionProvider>
      </GlobalProfileProvider>
    </PWAProvider>
  );
}

Key Benefits

1. Separation of Concerns

Each provider handles a specific responsibility:

  • PWAProvider: Core PWA state management
  • GlobalProfileProvider: Automatic profile data loading
  • SessionProvider: Authentication state
  • I18nProvider: Internationalization

2. Non-Invasive Integration

// Components just use the context - no middleware knowledge needed
const MyComponent = () => {
  const { profileEmail, currentAccount } = usePWAContext();
  // Profile data is automatically available!
  return <div>Welcome, {profileEmail}</div>;
};

3. Middleware-Like Behavior Without Middleware

The GlobalProfileProvider acts like middleware by:

  • Intercepting authentication state changes
  • Automatically triggering data loads
  • Handling errors globally
  • Preventing duplicate requests

4. Easy Testing and Debugging

// Test individual providers in isolation
<PWAProvider>
  <GlobalProfileProvider>
    <TestComponent />
  </GlobalProfileProvider>
</PWAProvider>

Comparison: Middleware vs Cascading Providers

Aspect Middleware Cascading Providers
Sitecore Safety ❌ Risk of conflicts ✅ No conflicts
PWA Compatibility ❌ May interfere with SW ✅ PWA-friendly
Testing ❌ Hard to isolate ✅ Easy unit testing
Debugging ❌ Complex stack traces ✅ Clear component tree
Team Collaboration ❌ Backend knowledge needed ✅ Frontend-only changes
Maintenance ❌ Framework updates risky ✅ Safe updates

Advanced Patterns

1. Conditional Provider Loading

const ConditionalProvider = ({ children, condition, fallback }) => {
  if (!condition) return fallback || children;
  return <SpecializedProvider>{children}</SpecializedProvider>;
};

2. Provider Composition

const AppProviders = ({ children }) => (
  <PWAProvider>
    <GlobalProfileProvider>
      <OfflineProvider>
        <ErrorBoundaryProvider>{children}</ErrorBoundaryProvider>
      </OfflineProvider>
    </GlobalProfileProvider>
  </PWAProvider>
);

3. Lazy Provider Loading

const LazyProfileProvider = lazy(
  () => import("./providers/GlobalProfileProvider")
);
// In _app.tsx
<Suspense fallback={<Loading />}>
  <LazyProfileProvider>{children}</LazyProfileProvider>
</Suspense>;

Best Practices

1. Keep Providers Focused

Each provider should have a single responsibility:

// ✅ Good - focused responsibility
const ProfileProvider = () => {
  /* handles only profile data */
};
const OfflineProvider = () => {
  /* handles only offline state */
};
// ❌ Bad - mixed responsibilities
const MegaProvider = () => {
  /* handles profile, offline, auth, etc. */
};

2. Use Reference Tracking for Side Effects

const hasTriggeredRef = useRef(false);
useEffect(() => {
  if (shouldTrigger && !hasTriggeredRef.current) {
    hasTriggeredRef.current = true;
    triggerAction();
  }
}, [shouldTrigger]);

3. Implement Proper Error Boundaries

const ProviderErrorBoundary = ({ children, onError }) => {
  // Handle provider-specific errors
  return <ErrorBoundary onError={onError}>{children}</ErrorBoundary>;
};

Performance Considerations

1. Memoization

const loadProfile = useCallback(async () => {
  // Expensive operation
}, [dependencies]); // Minimal dependencies

2. Selective Re-renders

const PWAContext = createContext();
const ProfileContext = createContext();
// Split contexts to prevent unnecessary re-renders

3. Lazy Evaluation

const value = useMemo(
  () => ({
    // Only compute when dependencies change
    expensiveValue: computeExpensiveValue(data),
  }),
  [data]
);

Final Thought on Cascading Provider Pattern

The Cascading Providers Pattern is a clean, modular alternative to modifying middleware in Next.js applications — especially those integrated with Sitecore and built as PWAs. It offers:

  • Safety – No risk of breaking Sitecore middleware or PWA features like service workers
  • Maintainability – Separation of concerns makes testing and debugging straightforward
  • Flexibility – Easy to add, remove, or refactor providers without affecting the core app
  • Performance – Optimized with memorization, lazy loading, and selective re-renders

For teams managing complex Sitecore integrations or PWA-specific workflows, Cascading Providers deliver middleware-like power without middleware-related risk — making it an ideal pattern for scalable, production-ready apps.

Additional Resources

Photo of Fishtank employee Sohrab Saboori

Sohrab Saboori

Senior Full-Stack Developer

Sohrab is a Senior Front-End Developer with extensive experience in React, Next.js, JavaScript, and TypeScript. Sohrab is committed to delivering outstanding digital solutions that not only meet but exceed clients' expectations. His expertise in building scalable and efficient web applications, responsive websites, and e-commerce platforms is unparalleled. Sohrab has a keen eye for detail and a passion for creating seamless user experiences. He is a problem-solver at heart and enjoys working with clients to find innovative solutions to their digital needs. When he's not coding, you can find him lifting weights at the gym, pounding the pavement on the run, exploring the great outdoors, or trying new restaurants and cuisines. Sohrab believes in a healthy and balanced lifestyle and finds that these activities help fuel his creativity and problem-solving skills.