Adding a Custom Context API for State Management in a Sitecore Headless Next.js Project
Managing state across pages in a Sitecore headless project can be challenging, especially when your application
relies on more than just Sitecore context data. Often, you'll need to integrate data from third-party APIs alongside
Sitecore content. Ensuring this diverse set of data is consistently accessible throughout your application requires an
effective state management solution. This is where a custom context API comes into play—it allows you to
centralize state management by integrating Sitecore data with additional sources, making the necessary information
readily available across all pages and components in your Next.js app.
In this blog, we’ll walk through setting up a custom context API to manage and persist state in your Sitecore
Next.js project. We’ll cover creating a context in the context
folder, integrating it within
app.tsx
to make it globally accessible, and using SWR for efficient data fetching. Finally, we'll explain
how to retain state across pages using Next Link
or JSS Link
for seamless navigation without
losing data.
Setting Up the Context API
1. Create the Context Folder
To keep your context files organized, start by creating a new folder called context
inside your
src
directory:
2. Implementing APPContext
We'll set up AppContext
to manage your application's state—in this case, the
username
.
Create a new file called AppContext.tsx
inside the context
folder and add the following
code:
// src/context/AppContext.tsx
import React, { createContext, useContext, useState, ReactNode } from 'react';
type AppContextType = {
username: string;
setUsername: (username: string) => void;
};
const AppContext = createContext<AppContextType | undefined>(undefined);
export const AppProvider = ({ children }: { children: ReactNode }) => {
const [username, setUsername] = useState<string>('');
return (
<AppContext.Provider value={{ username, setUsername }}>
{children}
</AppContext.Provider>
);
};
export const useAppContext = () => {
const context = useContext(AppContext);
if (!context) {
throw new Error('useAppContext must be used within an AppProvider');
}
return context;
};
Explanation of the Code:
- Type Definition (
AppContextType
):- Defines the structure of your context, specifying that it holds a
username
string and asetUsername
function.
- Defines the structure of your context, specifying that it holds a
- Creating the Context (
createContext
):- Initializes
AppContext
withundefined
to enforce usage within a provider.
- Initializes
AppProvider
Component:- Uses the
useState
hook to manage theusername
state. - Wraps
children
withAppContext.Provider
, passing downusername
andsetUsername
through thevalue
prop.
- Uses the
- Custom Hook (
useAppContext
):- Retrieves the context using
useContext(AppContext)
. - Throws an error if
useAppContext
is called outside of theAppProvider
, aiding in debugging.
- Retrieves the context using
Why Initialize with Undefined and Include Error Handling
By initializing AppContext
with undefined
, you ensure that any component attempting to
access the context outside of AppProvider
will receive an error. This helps catch configuration mistakes
early and ensures that the context is used correctly within your application.
Integrating AppContext into the Next.js App (app.tsx)
To make AppContext
available throughout your application, you need to wrap your main component with
AppProvider
in app.tsx
. This ensures that the context is accessible in all components,
providing a centralized state management solution.
// app.tsx
import type { AppProps } from 'next/app';
import { I18nProvider } from 'next-localization';
import { ExtendedSitecorePageProps } from 'lib/page-props';
import { AppProvider } from '../context/AppContext';
function App({ Component, pageProps }: AppProps<ExtendedSitecorePageProps>): JSX.Element {
const { dictionary, ...rest } = pageProps;
return (
<AppProvider>
<I18nProvider lngDict={dictionary} locale={pageProps.locale}>
<Component {...rest} />
</I18nProvider>
</AppProvider>
);
}
export default App;
Explanation:
- Import
AppProvider
:- We import
AppProvider
from our context to wrap our application.
- We import
- Wrap the Application:
- By wrapping
<Component />
with<AppProvider>
, we make the context available throughout the app.
- By wrapping
- Localization:
- The
I18nProvider
handles internationalization, using thedictionary
andlocale
frompageProps
.
- The
Integration with NextAuth for Authentication
In scenarios where you need to fetch user-specific information—like the username
—after
authentication, you can integrate next-auth
alongside your AppContext
. Here's how you can
modify app.tsx
to include authentication:
// app.tsx
import type { AppProps } from 'next/app';
import Router from 'next/router';
import { I18nProvider } from 'next-localization';
import { ExtendedSitecorePageProps } from 'lib/page-props';
import { SessionProvider } from 'next-auth/react';
import { AppProvider } from '../context/AppContext'; // Updated import
function App({ Component, pageProps }: AppProps<ExtendedSitecorePageProps>): JSX.Element {
const { dictionary, ...rest } = pageProps;
const isEdit = pageProps?.layoutData?.sitecore?.context?.pageEditing;
return (
<AppProvider>
<SessionProvider session={pageProps.session} refetchInterval={300}>
<I18nProvider lngDict={dictionary} locale={pageProps.locale}>
<Component {...rest} />
</I18nProvider>
</SessionProvider>
</AppProvider>
);
}
export default App;
Explanation:
- Import
SessionProvider
:- We import
SessionProvider
fromnext-auth/react
to handle user sessions and authentication.
- We import
- Wrap with
SessionProvider
:- We nest
SessionProvider
insideAppProvider
, allowing us to access authentication data within our context.
- We nest
- Authentication and Context Integration:
- By integrating authentication, you can fetch
user-specific data (like
username
) after login and store it in the context for global access.
- By integrating authentication, you can fetch
user-specific data (like
Tip: Always ensure that your context and authentication providers are correctly nested to prevent any access issues. The order typically is:
- AppProvider
- SessionProvider
- Other Providers (e.g., I18nProvider)
- Your Application Components
Using AppContext in Components
To use the context in your components or custom hooks, import useAppContext
and interact with the state
as needed.
Example: Custom Hook useUserDetails
Let's create a custom hook that fetches user details and updates the username
in the context.
// hooks/useUserDetails.ts
import { useEffect } from 'react';
import useSWR from 'swr';
import { useAppContext } from '../context/AppContext'; // Updated import
import { UserDetailsResponse } from '../types/type';
type UserDetails = {
userId: string;
};
const fetcher = async (url: string, arg: UserDetails) => {
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(arg),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
};
export const useUserDetails = (userDetails: UserDetails) => {
const { setUsername } = useAppContext();
const { data, error, isValidating } = useSWR<UserDetailsResponse, Error>(
['/api/user/details', userDetails],
([url, details]) => fetcher(url, details),
{
revalidateOnFocus: false,
revalidateOnReconnect: false,
fallbackData: undefined,
}
);
useEffect(() => {
if (data) {
setUsername(data.username || '');
}
}, [data, setUsername]);
return {
userDetails: data,
isUserDetailsLoading: isValidating,
userDetailsError: error,
};
};
Explanation:
- Import
useAppContext
:- Access the context to use
setUsername
.
- Access the context to use
- Update State on Data Fetch:
- After fetching the user details, update
username
in the context.
- After fetching the user details, update
- Effect Hook Dependencies:
- Include
setUsername
in the dependency array ofuseEffect
.
- Include
For more details about SWR and data fetching in Next.js, please refer to this comprehensive guide.
Using the Custom Hook in a Component
Now, let's see how to use the useUserDetails
hook and access the username
from the context
in a component.
// components/UserProfile.tsx
import React from 'react';
import { useUserDetails } from '../hooks/useUserDetails';
import { useAppContext } from '../context/AppContext';
const UserProfile = ({ userId }) => {
const { userDetails, isUserDetailsLoading, userDetailsError } = useUserDetails({ userId });
const { username } = useAppContext();
if (isUserDetailsLoading) return <div>Loading...</div>;
if (userDetailsError) return <div>Error loading user details.</div>;
return (
<div>
<h2>User Profile</h2>
<p>Username: {username}</p>
{/* Render other user details here */}
</div>
);
};
export default UserProfile;
Explanation:
- Access Username:
- Use
useAppContext
to accessusername
in the component.
- Use
- Display Data:
- Render the user profile, showing the username and other details.
Maintaining State Across Pages
Using Next Link or JSS Link for Navigation
To ensure that the state managed by AppContext
persists across page navigations, use client-side
routing:
- Next.js
Link
Component:
import Link from 'next/link';
const Navigation = () => (
<nav>
<Link href="/profile">Profile</Link>
<Link href="/settings">Settings</Link>
</nav>
);
- Sitecore JSS
Link
Component:
import { Link } from '@sitecore-jss/sitecore-jss-nextjs';
const Navigation = () => (
<nav>
<Link field={{ value: '/profile' }}>Profile</Link>
<Link field={{ value: '/settings' }}>Settings</Link>
</nav>
);
Explanation:
- Client-Side Navigation:
- Ensures the context state is preserved during navigation.
- Avoid Full Page Reloads:
- Using traditional
<a>
tags can cause the state to reset due to a full page reload.
- Using traditional
Additional Tips for State Persistence
- Persistent Storage:
- For state persistence across sessions or page reloads, consider using
localStorage
or cookies.
- For state persistence across sessions or page reloads, consider using
Final Thoughts on Enhancing State Management in Sitecore Next.js
Implementing a custom AppContext
provides a robust and scalable solution for managing global state in
your Sitecore headless Next.js application. By centralizing state management, you can seamlessly integrate data from
third-party APIs alongside Sitecore content, ensuring that essential information—like the username
in our example—is readily accessible across all pages and components.
We've walked through creating a context in the context
folder, integrating it within
app.tsx
to make it globally available, and using SWR for efficient data fetching. Additionally, we've
demonstrated how to maintain state across pages using client-side navigation with Next Link
or
JSS Link
, and how to integrate authentication using next-auth
for fetching user-specific
information.
This approach not only simplifies state management but also enhances the scalability and maintainability of your application. By avoiding prop drilling and keeping your state logic organized, you can focus on building features rather than managing data flow.
As you continue to develop your Sitecore headless Next.js projects, consider leveraging the power of React Context for your state management needs. It's a straightforward yet powerful tool that can significantly improve the efficiency and user experience of your applications.