I have a react app that uses mui v5, notistack v3 and react-router-dom v6. I have wrapped the react-router-dom RouterProvider component with notistack's SnackbarProvider component. In my AdminLayout, I have a useEffect that listens to messages on a websocket and displays the message with notistacks snackbar. The AdminLayout is a parent element in my router and has a child route (component) called Project. In my Project component, I am using a mui drawer component. The drawer is only visible when the showComments state is true. The problem is that this snackbar will only appear inside of the drawer, which means that if the showComments state is set to false, the snackbar wont show. How to show the snackbar even if the drawer is closed?
Here's what's happening: Notistack Issue Video
Below are all the component code snippets:
App.js:
import { useMemo, useState } from "react";
import reactLogo from "./assets/react.svg";
import viteLogo from "/vite.svg";
import "./App.css";
import "react-image-gallery/styles/css/image-gallery.css";
import { RouterProvider } from "react-router-dom";
// import router from "./routes.jsx";
import { ColorContext } from "@/contexts/ColorContext.js";
import { CssBaseline, ThemeProvider } from "@mui/material";
import { createTheme } from "@/themes";
import { createBrowserRouter, useNavigate } from "react-router-dom";
import AdminLayout from "@/layouts/Admin";
import AuthLayout from "./layouts/Auth";
import Login from "@/pages/auth/login";
import Register from "./pages/auth/register";
import * as Sentry from "@sentry/react";
import axios from "axios";
import Projects from "@/pages/projects";
import CreateProject from "@/pages/projects/create";
import useAuth from "./hooks/useAuth";
import { SnackbarProvider } from "notistack";
function App() {
const theme = createTheme();
const { token } = useAuth();
const router = createBrowserRouter([
{
element: <AuthLayout />,
children: [
{
path: "/login",
element: <Login />,
},
{
path: "/register",
loader: async () => {
try {
const resp = await axios.get(
"http://localhost:5000/api/user/usernames"
);
return resp.data.usernames ?? [];
} catch (error) {
console.log(error.message);
Sentry.captureMessage(error.message, "error");
return [];
}
},
element: <Register />,
},
],
},
{
path: "/",
element: <AdminLayout />,
children: [
{
path: "/projects",
loader: async () => {
try {
const resp = await axios.get("http://localhost:5000/projects", {
headers: { Authorization: `bearer ${token}` },
});
if (resp.status !== 204) {
return resp.data.projects
? resp.data.projects
: resp.data.project;
}
return null;
} catch (error) {
console.log(error.message);
Sentry.captureMessage(error.message, "error");
return [];
}
},
element: <Projects />,
},
{
path: "/projects/register",
loader: async () => {
try {
const resp = await axios.get("http://localhost:5000/projects", {
headers: { Authorization: `bearer ${token}` },
});
if (resp.status !== 204) {
return true;
}
return null;
} catch (error) {
console.log(error.message);
Sentry.captureMessage(error.message, "error");
return [];
}
},
element: <CreateProject />,
},
],
},
]);
return (
<ThemeProvider theme={theme}>
<CssBaseline enableColorScheme />
<SnackbarProvider
anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
>
<RouterProvider router={router} />
</SnackbarProvider>
</ThemeProvider>
);
}
export default App;
AdminLayout/index.jsx:
import { useCallback, useEffect, useState, useRef } from "react";
import SideNav from "./SideNav";
import TopBar from "./TopBar";
import { Outlet, useLocation } from "react-router-dom";
import { styled } from "@mui/material/styles";
import withAuthGuard from "@/hoc/withAuthGuard";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import useAuth from "@/hooks/useAuth";
import { useDispatch, useSelector } from "react-redux";
import { finishJob } from "@/app/queueSlice";
const SIDE_NAV_WIDTH = 280;
const LayoutRoot = styled("div")(({ theme }) => ({
display: "flex",
flex: "1 1 auto",
maxWidth: "100%",
[theme.breakpoints.up("lg")]: {
paddingLeft: SIDE_NAV_WIDTH,
},
}));
const LayoutContainer = styled("div")({
display: "flex",
flex: "1 1 auto",
flexDirection: "column",
width: "100%",
});
const AdminLayout = withAuthGuard(() => {
const [openNav, setOpenNav] = useState(false);
const [showComments, setShowComments] = useState(false);
const pathname = useLocation().pathname;
const { email } = useAuth();
const dispatch = useDispatch();
const job = useSelector((state) => state.queue.jobId);
const handlePathnameChange = useCallback(() => {
if (openNav) {
setOpenNav(false);
}
}, [openNav]);
useEffect(() => {
handlePathnameChange();
}, [pathname]);
const socketRef = useRef(null);
useEffect(() => {
const socket = new WebSocket("ws://localhost:8080/");
socket.addEventListener("open", () => {
console.log("WebSocket connection opened");
});
socket.addEventListener("message", (event) => {
const { msg, success } = JSON.parse(event.data);
if (success) {
if (msg.includes(email)) {
enqueueSnackbar(msg, { variant: "info" });
if (job) {
dispatch(finishJob());
}
}
} else {
enqueueSnackbar(msg, { variant: "warning" });
dispatch(finishJob());
}
});
socketRef.current = socket;
return () => {
socket.close();
};
}, [job]);
return (
<>
<TopBar onNavOpen={() => setOpenNav(true)} open={showComments} />
<SideNav onClose={() => setOpenNav(false)} open={openNav} />
<LayoutRoot>
<LayoutContainer>
<Outlet context={[showComments, setShowComments]} />
</LayoutContainer>
</LayoutRoot>
</>
);
});
export default AdminLayout;
Project.jsx:
import React, { useEffect, useState } from "react";
import {
useLoaderData,
useLocation,
useNavigate,
Link,
useOutletContext,
} from "react-router-dom";
import {
Container,
Stack,
Button,
ButtonGroup,
Typography,
Divider,
Chip,
Card,
Grid,
List,
ListItem,
Link as MuiLink,
useMediaQuery,
Drawer,
Backdrop,
CircularProgress,
} from "@mui/material";
import { styled } from "@mui/material/styles";
import { SnackbarProvider, enqueueSnackbar } from "notistack";
import { Scrollbar } from "@/components/Scrollbar";
import useAuth from "@/hooks/useAuth";
import ImageGallery from "react-image-gallery";
import axios from "axios";
import { useDispatch, useSelector } from "react-redux";
import { createJob } from "@/app/queueSlice";
import Comments from "@/components/comments";
import Reply from "@/components/comments/reply.component";
const Projects = () => {
const navigate = useNavigate();
const projects = useLoaderData();
const [showComments, setShowComments] = useOutletContext();
const location = useLocation();
const { role, token } = useAuth();
const jobs = useSelector((state) => state.queue.jobId);
const dispatch = useDispatch();
const lgUp = useMediaQuery((theme) => theme.breakpoints.up("lg"));
const [comments, setComments] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
const getComments = async () => {
setLoading(true);
const resp = await axios.get(
`http://localhost:5000/comments/${projects.projectId}`,
{ headers: { Authorization: `Bearer ${token}` } }
);
if (resp.status === 200) {
setComments(resp.data.comments);
// console.log(resp.data.comments);
setLoading(false);
} else {
enqueueSnackbar(
"Something went wrong while trying to fetch comments, Please refresh the page!",
"error"
);
setLoading(false);
}
};
getComments();
}, []);
useEffect(() => {
if (location.state) {
setMsg(location.state.msg);
}
}, []);
useEffect(() => {
if (!projects) {
navigate("/projects/register", {
state: { msg: "Please register a project!" },
});
}
}, []);
const handleExport = async () => {
try {
if (!jobs) {
const resp = await axios.put(
`http://localhost:5000/projects/export/${projects.projectId}`,
null,
{
headers: { Authorization: `bearer ${token}` },
}
);
if (resp.status === 200) {
enqueueSnackbar("Export job started!", { variant: "success" });
dispatch(createJob(resp.data.job));
}
} else {
enqueueSnackbar(
"Export job already triggered, please wait for it to complete!",
{ variant: "error" }
);
}
} catch (error) {
console.log(error);
}
};
const Main = styled("main", { shouldForwardProp: (prop) => prop !== "open" })(
({ theme, open }) => ({
flexGrow: 1,
padding: theme.spacing(3),
...(open
? {
transition: theme.transitions.create("all", {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.leavingScreen,
}),
marginRight: "35vw",
}
: {
transition: theme.transitions.create("all", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
marginRight: 0,
}),
position: "relative",
})
);
return (
<Scrollbar>
<Main open={showComments}>
{role === "user" && projects && (
<Stack spacing={2}>
<Stack
direction={lgUp ? "row" : "column"}
alignItems="center"
justifyContent="space-between"
spacing={lgUp ? 0 : 2}
sx={{
padding: "0 2rem",
}}
>
<Stack spacing={1}>
<Stack direction="row" spacing={2} alignItems="center">
<Typography variant="h4">{projects.projectName}</Typography>
<Chip label={projects.projectStatus} />
</Stack>
<Stack spacing={1} direction="row">
{projects.projectCategory.map((category) => (
<Chip
key={category}
sx={{ textTransform: "capitalize" }}
label={category}
color="secondary"
/>
))}
</Stack>
</Stack>
<ButtonGroup variant="contained" size="medium">
<Button
onClick={handleExport}
variant="contained"
color="secondary"
>
Export PDF
</Button>
<Button
onClick={() => {
enqueueSnackbar("Hey");
}}
variant="contained"
color="warning"
>
Edit
</Button>
<Button
onClick={() => setShowComments(!showComments)}
variant="contained"
color="primary"
>
{showComments ? "Hide Comments" : "View Comments"}
</Button>
</ButtonGroup>
</Stack>
<Divider variant="middle">
<Chip label="Project Description" color="primary" size="medium" />
</Divider>
<Card sx={{ padding: "2rem" }}>
<Typography variant="body1">{projects.projectDesc}</Typography>
</Card>
<Divider variant="middle">
<Chip label="Project Gallery" color="primary" size="medium" />
</Divider>
<Card sx={{ padding: "2rem" }}>
<ImageGallery
items={projects.projectImages.map((img) => ({
original: img,
thumbnail: img,
thumbnailHeight: "75px",
thumbnailWidth: "75px",
originalHeight: "500px",
}))}
showFullscreenButton={false}
showPlayButton={false}
autoPlay
showNav
/>
</Card>
<Divider variant="middle">
<Chip label="Other Information" color="primary" size="medium" />
</Divider>
<Card sx={{ padding: "2rem" }}>
<Grid container spacing={2}>
<Grid item md={4} xs={12}>
<Typography variant="h6">Organization Details</Typography>
<Typography variant="subtitle1">
{projects.organizationName}
</Typography>
<Typography variant="body1">
{projects.organizationAddress}
</Typography>
</Grid>
<Grid item md={4} xs={12}>
<Typography variant="h6">Other Links</Typography>
<List>
{projects.links.length > 0
? projects.links.map((link, index) => (
<ListItem key={index}>
<MuiLink
component={Link}
to={link}
underline="hover"
variant="subtitle2"
>
{link}
</MuiLink>
</ListItem>
))
: `-`}
</List>
</Grid>
<Grid item md={4} xs={12}>
<Typography variant="h6">Project Created On</Typography>
<Typography variant="body1">
{new Date(
projects.createdAt.substring(
0,
projects.createdAt.lastIndexOf("T")
)
).toDateString()}
</Typography>
</Grid>
</Grid>
</Card>
</Stack>
)}
</Main>
<Drawer
sx={{
width: lgUp ? "35vw" : "100vw",
flexShrink: 0,
"& .MuiDrawer-paper": {
width: lgUp ? "35vw" : "100vw",
},
position: "relative",
}}
anchor="right"
variant="persistent"
open={showComments}
>
<Backdrop
open={loading}
sx={{
color: "#fff",
zIndex: (theme) => theme.zIndex.drawer + 1,
position: "absolute",
}}
>
<CircularProgress />
</Backdrop>
<Container role="main" maxWidth="md" sx={{ marginTop: "64px" }}>
<Scrollbar>
<Stack
direction="column"
justifyContent="space-between"
alignItems="center"
>
{comments
.filter((comment) => comment.parent === null)
?.map((comment) => (
<React.Fragment key={comment._id}>
<Comments
{...comment}
setLoading={setLoading}
setComments={setComments}
/>
{comments.filter(
(childComment) => comment._id === childComment.parent
).length > 0 && (
<Grid container>
<Grid
item
xs={1}
sx={{
borderLeft: "2px solid #E9EBF0",
transform: "translate(50%, -10px)",
}}
></Grid>
<Grid item xs={11}>
{comments.length > 0 &&
comments
.filter(
(childComment) =>
comment._id === childComment.parent
)
.map((childComment) => (
<Comments
{...childComment}
replyingTo={comment.commenterName}
key={childComment._id}
setLoading={setLoading}
setComments={setComments}
/>
))}
</Grid>
</Grid>
)}
</React.Fragment>
))}
</Stack>
<Reply
sendReply="Send"
projectId={projects.projectId}
parent={null}
setLoading={setLoading}
setComments={setComments}
/>
</Scrollbar>
</Container>
</Drawer>
</Scrollbar>
);
};
export default Projects;