I have implemented the following Context in my Next React web app in order to login to a remote REST API:
"use client";
import { createContext, useState, useEffect, useContext } from "react";
import { useRouter, usePathname } from "next/navigation";
import { useDialog } from "@/context/DialogContext";
const AuthContext = createContext({
user: null,
login: () => {},
logout: () => {},
loading: true,
});
export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const router = useRouter();
const pathname = usePathname();
const { showNotification } = useDialog();
const checkSession = async () => {
try {
const res = await fetch("/api/login", {
credentials: "include",
});
if (res.ok) {
const data = await res.json();
setUser(data);
} else {
setUser(null);
showNotification("Sesión expirada", "error");
router.push("/plataforma/login"); // Redirigir si la sesión expira
}
} catch (error) {
console.error("Error validando sesión", error);
setUser(null);
router.push("/plataforma/login");
} finally {
setLoading(false);
}
};
useEffect(() => {
checkSession(); // Validar en carga inicial
const interval = setInterval(checkSession, 300000); // 5 minutos
return () => clearInterval(interval);
}, []);
useEffect(() => {
checkSession();
}, [pathname]);
const login = async (email, password) => {
const res = await fetch("api/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({ email, password }),
});
const data = await res.json();
if (res.ok) {
setUser(data);
router.push("/plataforma/years"); // Redirigir tras login
} else {
throw new Error(data.error);
}
};
const logout = async () => {
alert("Logout");
try {
const res = await fetch("/api/logout", {
method: "POST",
credentials: "include",
});
console.log('Logout response:', res);
setUser(null);
router.push("/plataforma/login");
} catch (error) {
console.error('Logout error:', error);
}
};
return (
<AuthContext.Provider value={{ user, login, logout, loading }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => useContext(AuthContext);
And I use this context in my main layout.js as follows:
"use client";
import { useState, useEffect } from "react";
import { useAuth } from "@/context/AuthContext"; // Nuevo AuthContext para autenticación segura
import { AppRouterCacheProvider } from "@mui/material-nextjs/v15-appRouter";
import { ThemeProvider } from "@mui/material/styles";
import theme from "@/theme";
import { useRouter } from "next/navigation";
import Image from "next/image";
import { DialogProvider } from "@/context/DialogContext";
import { AuthProvider } from "@/context/AuthContext";
import HelpDrawer from "@/components/HelpDrawer";
import {
Box,
Button,
Drawer,
AppBar,
CssBaseline,
Toolbar,
List,
Typography,
Divider,
ListItem,
ListItemButton,
ListItemIcon,
ListItemText,
IconButton,
} from "@mui/material";
import CalendarMonthIcon from "@mui/icons-material/CalendarMonth";
import FlightTakeoffIcon from "@mui/icons-material/FlightTakeoff";
import HomeIcon from "@mui/icons-material/Home";
import MenuIcon from "@mui/icons-material/Menu";
import SettingsIcon from "@mui/icons-material/Settings";
import HelpOutlineIcon from "@mui/icons-material/HelpOutline";
import LogoutIcon from "@mui/icons-material/Logout";
import PersonIcon from "@mui/icons-material/Person";
const drawerWidth = 250;
export default function RootLayout({ children }) {
const router = useRouter();
const { user, loading, logout } = useAuth(); // Estado de autenticación
console.log("Current user:", user); // Debugging statement
const [mobileOpen, setMobileOpen] = useState(false);
const [isClosing, setIsClosing] = useState(false);
const [helpDrawerOpen, setHelpDrawerOpen] = useState(false);
const handleDrawerToggle = () => {
if (!isClosing) {
setMobileOpen(!mobileOpen);
}
};
const handleDrawerClose = () => {
setIsClosing(true);
setMobileOpen(false);
};
const handleDrawerTransitionEnd = () => {
setIsClosing(false);
};
const handleHelpDrawerToggle = () => {
setHelpDrawerOpen(!helpDrawerOpen);
};
const menuItems = [
{ text: "Inicio", icon: <HomeIcon />, path: "/plataforma" },
{ text: "Personal", icon: <PersonIcon />, path: user ? `/plataforma/users/edit/${user.id}` : "#" }, // Handle null user
{ text: "Años", icon: <CalendarMonthIcon />, path: "/plataforma/years" },
{ text: "Viajes", icon: <FlightTakeoffIcon />, path: "/plataforma/trips" },
];
const bottomMenuItems = [
{ text: "Configuración", icon: <SettingsIcon />, path: "/plataforma/settings" },
{ text: "Ayuda", icon: <HelpOutlineIcon />, path: "/plataforma/help" },
];
// Redirigir a login si no está autenticado
useEffect(() => {
if (!loading && !user) {
router.push("/plataforma/login");
} else {
console.log("User inside useEffect:", user); // Debugging statement
}
}, [user, loading, router]);
const drawerContent = (
<Box sx={{ display: "flex", flexDirection: "column", height: "100%" }}>
<Toolbar />
<Divider />
{/* Mostrar nombre de usuario */}
{user && (
<Box sx={{ padding: 2, textAlign: "center" }}>
<Typography variant="h6">Usuario: {user.name || "Nombre no disponible"}</Typography>
</Box>
)}
<Divider />
<List sx={{ color: theme.palette.secondary.main }}>
{menuItems.map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton onClick={() => router.push(item.path)}>
<ListItemIcon sx={{ color: theme.palette.secondary.main }}>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider sx={{ mt: "auto" }} />
<List sx={{ color: theme.palette.secondary.main }}>
{bottomMenuItems.map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton onClick={() => router.push(item.path)}>
<ListItemIcon sx={{ color: theme.palette.secondary.main }}>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />
</ListItemButton>
</ListItem>
))}
</List>
<Divider />
<List sx={{ color: theme.palette.secondary.main }}>
<ListItem key="logout" disablePadding>
<ListItemButton onClick={() => { logout(); }}>
<ListItemIcon sx={{ color: theme.palette.secondary.main }}>{<LogoutIcon />}</ListItemIcon>
<ListItemText primary="Cerrar sesión" />
</ListItemButton>
</ListItem>
</List>
</Box>
);
return (
<html lang="es">
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}>
<DialogProvider>
<AuthProvider>
<Box sx={{ display: "flex" }}>
<CssBaseline />
<AppBar position="fixed" sx={{ backgroundColor: theme.palette.secondary.main, zIndex: (theme) => theme.zIndex.drawer + 1 }}>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
sx={{ mr: 2, display: { sm: "none" } }}
>
<MenuIcon />
</IconButton>
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "left", flexGrow: 1 }}>
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
7p
</Typography>
<Image src="/i/easy50.png" alt="Logo" width={50} height={50} priority />
<Typography variant="h6" sx={{ fontWeight: "bold" }}>
facil.es
</Typography>
</Box>
<Box sx={{ marginLeft: "auto" }}>
<IconButton
color="inherit"
aria-label="open help drawer"
edge="end"
onClick={handleHelpDrawerToggle}
>
<HelpOutlineIcon />
</IconButton>
</Box>
</Toolbar>
</AppBar>
<Drawer
variant="temporary"
open={mobileOpen}
onTransitionEnd={handleDrawerTransitionEnd}
onClose={handleDrawerClose}
sx={{
display: { xs: "block", sm: "none" },
"& .MuiDrawer-paper": { boxSizing: "border-box", width: drawerWidth },
}}
>
{drawerContent}
</Drawer>
<Drawer
variant="permanent"
sx={{
width: drawerWidth,
flexShrink: 0,
display: { xs: "none", sm: "block" },
"& .MuiDrawer-paper": { width: drawerWidth, boxSizing: "border-box" },
}}
>
{drawerContent}
</Drawer>
<Box component="main" sx={{ flexGrow: 1, p: 2, overflowX: "hidden", width: "100vw" }}>
<Toolbar />
{children}
</Box>
<HelpDrawer open={helpDrawerOpen} onClose={handleHelpDrawerToggle} />
</Box>
</AuthProvider>
</DialogProvider>
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
);
}
The issue is that user
from useAuth
is always null, so I am not able to the the user name logegd in (user.name
) or even call user.logout()
. I checked the calls to the API in AuthContext and is working fine (user data is set). Any suggestions?
The useAuth can only be called by components that are inside your AuthProvider
.
In your case I'd do something like this:
layout.js
return (
<html lang="es">
<body>
<AppRouterCacheProvider options={{ enableCssLayer: true }}>
<ThemeProvider theme={theme}>
<DialogProvider>
<AuthProvider>
<BaseLayout>
{children}
</BaseLayout>
</AuthProvider>
</DialogProvider>
</ThemeProvider>
</AppRouterCacheProvider>
</body>
</html>
)
And then inside your new Baselayout component you'd have the rest of your logic and user management. Your drawers and headers.
Inside the Baselayout and the {children}
(which would be your pages) you can use your hook.