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?
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?
1 Answer
Reset to default 2The 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.