Implementing a Custom Context API for State Management in Sitecore Headless with Next.js

Enhancing state management in Sitecore Next.js

November 15, 2024

By Sohrab Saboori

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:

Visual Studio Code file structure with AppContext.tsx highlighted under context folder.

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 a setUsername function.
  • Creating the Context (createContext):
    • Initializes AppContext with undefined to enforce usage within a provider.
  • AppProvider Component:
    • Uses the useState hook to manage the username state.
    • Wraps children with AppContext.Provider, passing down username and setUsername through the value prop.
  • Custom Hook (useAppContext):
    • Retrieves the context using useContext(AppContext).
    • Throws an error if useAppContext is called outside of the AppProvider, aiding in debugging.

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.
  • Wrap the Application:
    • By wrapping <Component /> with <AppProvider>, we make the context available throughout the app.
  • Localization:
    • The I18nProvider handles internationalization, using the dictionary and locale from pageProps.

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 from next-auth/react to handle user sessions and authentication.
  • Wrap with SessionProvider:
    • We nest SessionProvider inside AppProvider, allowing us to access authentication data within our context.
  • 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.

Tip: Always ensure that your context and authentication providers are correctly nested to prevent any access issues. The order typically is:

  1. AppProvider
  2. SessionProvider
  3. Other Providers (e.g., I18nProvider)
  4. 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.
  • Update State on Data Fetch:
    • After fetching the user details, update username in the context.
  • Effect Hook Dependencies:
    • Include setUsername in the dependency array of useEffect.

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 access username in the component.
  • Display Data:
    • Render the user profile, showing the username and other details.

Maintaining State Across Pages

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.

Additional Tips for State Persistence

  • Persistent Storage:
    • For state persistence across sessions or page reloads, consider using localStorage or cookies.

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.

References

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.