reactjs - NextJS: Valores de contexto indefinidos en producción (funciona bien en desarrollo)
Se ha implementado una función de «modo oscuro» en mi aplicación Next.js utilizando Context API de React.
Todo funciona bien durante el desarrollo, sin embargo, han surgido problemas relacionados con el Context provider en la versión compilada: los estados globales aparecen como undefined y no se pueden manejar.
_app.tsx está envuelto con el ThemeProvider de la siguiente manera:
// 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;
El ThemeContext exporta correctamente tanto su Provider como su 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>
);
};
El ThemeToggler responsable de actualizar el estado darkMode funciona correctamente durante el desarrollo (el tema se cambia y el valor correcto se console.log al hacer clic), sin embargo, no hace nada durante la producción (los console.log muestran un estado undefined):
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;
Las soluciones/sugerencias que he buscado antes de publicar esto no fueron de utilidad.
API de Contexto de React indefinida en producción — react y react-dom están en la misma versión.
Gracias de antemano.
P.D. Para quienes se preguntan por qué estoy usando tanto Redux como Context para la gestión del estado global:
Contextes más adecuado para actualizaciones de estado poco frecuentes y simples, como temas y autenticación.Reduxes mejor para actualizaciones de estado de alta frecuencia y complejas, además de proporcionar una mejor herramienta de depuración:Redux DevTools.
P.D.2 Sí, es mejor - en términos de rendimiento - instalar las dependencias de FontAwesome en lugar de usar un CDN.
Respuesta #1
Gracias por compartir el código. Está bien escrito. Al leerlo, no veo ningún problema. Basado en la topología de tu componente, mientras tu ThemeToggler esté definido bajo cualquier componente de página, tu darkMode no puede ser undefined.
Aquí está tu topología del sitio
<MyApp>
<Provider>
// A. will not work
<ThemeProvider>
<HomePage>
// B. should work
</HomePage>
</ThemeProvider>
// C. will not work
</Provider>
</MyApp>
Aunque tu ThemeProvider es un proveedor personalizado, dentro de ThemeContext.Provider está definido con el valor {{ darkMode, toggleDarkMode }}. Así que, en teoría, no puedes obtener undefined a menos que tu componente ThemeToggler no esté bajo un componente HomePage. Marqué dos ubicaciones que no funcionan; cualquier componente colocado en la ubicación A o C te dará undefined.
Dado que tienes una condición para HomePage, puedes encontrarte con este problema si estás en otras páginas. Así que, en general, deberías envolver el ThemeProvider encima de tu enrutador.
<ThemeProvider>
<AuthProvider>
{Component.name != "PageNotFound" && (
<Component {...pageProps} />
)}
</AuthProvider>
</ThemeProvider>
Entiendes el punto, primero quieres pasar por una capa donde ese tema siempre exista antes de activar un enrutador.
Puedes confirmar si este es el caso haciendo la siguiente prueba.
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<AuthProvider>
<Component {...pageProps} />
</AuthProvider>
</ThemeProvider>
)
}
Si esto funciona en producción, entonces lo confirma. Para ser honesto, este problema también existe en desarrollo, sin embargo, tal vez debido a que tus rutas cambian demasiado rápido, normalmente estos problemas se ocultan.
Respuesta #2
El problema es que no hay Window ni localStorage en el servidor en Next.js. Así que cuando intentas obtener el valor de localStorage tu contexto queda indefinido y luego, en el componente del cliente en useEffect, puedes obtener el valor correcto. Resolví este problema estableciendo un valor por defecto en la asignación por desestructuración de la siguiente manera:
const [tema='oscuro', setTema]=useLocalStorage('tema', 'oscuro').
Pero aún así, el valor no se pasa a localStorage en la primera carga de la página, sin embargo, la aplicación funciona correctamente incluso de esa manera.
