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.
<App>
<Component />
</App>
<PWAProvider>
<GlobalProfileProvider>
<AuthenticationProvider>
<App>
<Component />
</App>
</AuthenticationProvider>
</GlobalProfileProvider>
</PWAProvider>
Implementation Example
Here's how we implemented this pattern in our PWA:
1. Base PWA Context Provider
const PWAProvider = ({ children }) => {
const [profileEmail, setProfileEmail] = useState("");
const [currentAccount, setCurrentAccount] = useState(null);
const [isOffline, setIsOffline] = useState(false);
const value = {
profileEmail,
setProfileEmail,
currentAccount,
setCurrentAccount,
isOffline,
};
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
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(() => {
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
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
const MyComponent = () => {
const { profileEmail, currentAccount } = usePWAContext();
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
<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")
);
<Suspense fallback={<Loading />}>
<LazyProfileProvider>{children}</LazyProfileProvider>
</Suspense>;
Best Practices
1. Keep Providers Focused
Each provider should have a single responsibility:
const ProfileProvider = () => {
};
const OfflineProvider = () => {
};
const MegaProvider = () => {
};
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 }) => {
return <ErrorBoundary onError={onError}>{children}</ErrorBoundary>;
};
1. Memoization
const loadProfile = useCallback(async () => {
}, [dependencies]);
2. Selective Re-renders
const PWAContext = createContext();
const ProfileContext = createContext();
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