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 backenduseSession()
: Comes fromnext-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 managementGlobalProfileProvider
: Automatic profile data loadingSessionProvider
: Authentication stateI18nProvider
: 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.