Home > Enterprise >  NextJS: Context values undefined in production (works fine in development)
NextJS: Context values undefined in production (works fine in development)

Time:11-22

A "dark mode" feature has been implemented on my Next.js application using React's Context api.

Everything works fine during development, however, Context provider-related problems have arisen on the built version — global states show as undefined and cannot be handled.


_app.tsx is wrapped with the ThemeProvider as such:

// React & Next hooks
import React, { useEffect } from "react";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";

// Irrelevant imports

// Global state management
import { Provider } from "react-redux";
import store from "../redux/store";
import { AuthProvider } from "../context/UserContext";
import { ThemeProvider } from "../context/ThemeContext";

// Components
import Layout from "../components/Layout/Layout";
import Footer from "../components/Footer/Footer";

// Irrelevant code

function MyApp({ Component, pageProps }: AppProps) {
  const router = useRouter();

  // Applying different layouts depending on page
  switch (Component.name) {
    case "HomePage":
      return (
        <Provider store={store}>
          <ThemeProvider>
            <AuthProvider>
              <Component {...pageProps} />
              <Footer color="fff" />
            </AuthProvider>
          </ThemeProvider>
        </Provider>
      );
    case "PageNotFound":
      return (
        <>
          <Component {...pageProps} />
          <Footer color="#f2f2f5" />
        </>
      );

    default:
      // Irrelevant code
  }
}
export default MyApp;

The ThemeContext correctly exports both its Provider and Context:

import { createContext, ReactNode, useState, useEffect } from "react";

type themeContextType = {
  darkMode: boolean | null;
  toggleDarkMode: () => void;
};

type Props = {
  children: ReactNode;
};

// Checks for user's preference.
const getPrefColorScheme = () => {
  return !window.matchMedia
    ? null
    : window.matchMedia("(prefers-color-scheme: dark)").matches;
};

// Gets previously stored theme if it exists.
const getInitialMode = () => {
  const isReturningUser = "dark-mode" in localStorage; // Returns true if user already used the website.
  const savedMode = localStorage.getItem("dark-mode") === "true" ? true : false;
  const userPrefersDark = getPrefColorScheme(); // Gets user's colour preference.

  // If mode was saved ► return saved mode else get users general preference.
  return isReturningUser ? savedMode : userPrefersDark ? true : false;
};

export const ThemeContext = createContext<themeContextType>(
  {} as themeContextType
);

export const ThemeProvider = ({ children }: Props) => {
  // localStorage only exists on the browser (window), not on the server
  const [darkMode, setDarkMode] = useState<boolean | null>(null);

  // Getting theme from local storage upon first render
  useEffect(() => {
    setDarkMode(getInitialMode);
  }, []);

  // Prefered theme stored in local storage
  useEffect(() => {
    localStorage.setItem("dark-mode", JSON.stringify(darkMode));
  }, [darkMode]);

  const toggleDarkMode = () => {
    setDarkMode(!darkMode);
  };

  return (
    <ThemeContext.Provider value={{ darkMode, toggleDarkMode }}>
      {children}
    </ThemeContext.Provider>
  );
};

The ThemeToggler responsible for updating the darkMode state operates properly during development (theme toggled and correct value console.loged upon clicking), however it doesn't do anything during production (console.logs an undefined state):

import React, { FC, useContext } from "react";
import { ThemeContext } from "../../context/ThemeContext";

const ThemeToggler: FC = () => {
  const { darkMode, toggleDarkMode } = useContext(ThemeContext);

  const toggleTheme = () => {
    console.log(darkMode) // <--- darkMode is undefined during production
    toggleDarkMode();
  };
  return (
    <div className="theme-toggler">
      <i
        className={`fas ${darkMode ? "fa-sun" : "fa-moon"}`}
        data-testid="dark-mode"
        onClick={toggleTheme}
      ></i>
    </div>
  );
};

export default ThemeToggler;

The solutions/suggestions I've looked up before posting this were to no avail.

React Context API undefined in productionreact and react-dom are on the same version.

Thanks in advance.


P.S. For those wondering why I am using both Redux and Context for global state management:

  • Context is best suited for low-frequency and simple state updates such as themes and authentication.

  • Redux is better for high-frequency and complex state updates in addition to providing a better debugging tool — Redux DevTools.

P.S.2 Yes, it is better – performance-wise – to install FontAwesome's dependencies rather than use a CDN.

CodePudding user response:

Thanks for sharing the code. It's well written. By reading it i don't see any problem. Based on your component topology, as long as your ThemeToggler is defined under any page component, your darkMode can't be undefined.

Here's your topology of the site

  <MyApp>
    <Provider>
      // A. will not work
      <ThemeProvider>
        <HomePage>
          // B. should work
        </HomePage>
      </ThemeProvider>
      // C. will not work
    </Provider>
  </MyApp>
       
        

Although your ThemeProvider is a custom provider, inside ThemeContext.Provider is defined with value {{ darkMode, toggleDarkMode }}. So in theory you can't get undefined unless your component ThemeToggler is not under a HomePage component. I marked two non working locations, any component put under location A or C will give you undefined.

Since you have a condition for HomePage, you can run into this problem if you are on other pages. So in general you should wrap the ThemeProvider on top of your router.

  <ThemeProvider>
    <AuthProvider>
      {Component.name != "PageNotFound" && (
         <Component {...pageProps} />
      )}
    </AuthProvider>
  </ThemeProvider>

You get the point, you want to first go through a layer that theme always exist before you fire up a router.

You can confirm if this is the case by doing the following test.

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <ThemeProvider>
      <AuthProvider>
         <Component {...pageProps} />
      </AuthProvider>
    </ThemeProvider>
  )
}

If this works in production, then it confirms it. To be honest, this problem also exists in the dev, however maybe due to your routes change too quickly, it normally hides these issues.

  • Related