Idiomatic way to handle Message Alerts state in React and Material-UI - reactjs

I basically am building a web application using react and material UI. I have a registration form.
When this registration form successfully submits, it should display a success message at the top of the page.
Now, I have cordoned off the top of every page to show the MyAlert component. I have done this to make my site more consistent. Basically whenever there is a message to show to the user, it will render there.
Now, the issue is once the message is shown, if I navigate around my site, there is no way to turn it off.
Could someone please help explain what the best way to show a message once, then hide it would be? Basically once the user registers successfully, show a success message. Once the user navigates to a new page, hide the message at the top until a new message needs to be shown.
I imagine this is a very common design pattern, so I must be missing something in terms of React knowledge. Even best practice design patterns, or links to how it can be handled would be appreciated.
My toy app looks as follows:
App.jsx
import * as React from 'react'
import { useState, useEffect } from 'react'
import { BrowserRouter, Switch, Route } from 'react-router-dom'
import { Link } from 'react-router-dom'
import { Alert } from '#material-ui/lab'
import MenuIcon from '#material-ui/icons/Menu'
import {
Button,
AppBar,
Toolbar,
IconButton,
Typography,
makeStyles,
Container,
} from '#material-ui/core'
const useStyles = makeStyles((theme) => ({
root: {
flexGrow: 1,
},
menuButton: {
marginRight: theme.spacing(2),
},
title: {
flexGrow: 1,
},
}))
const Navbar = (props) => {
const classes = useStyles()
return (
<div>
<AppBar position="static">
<Toolbar>
<IconButton
edge="start"
className={classes.menuButton}
color="inherit"
aria-label="menu"
>
<MenuIcon />
</IconButton>
<Typography variant="h6" className={classes.title}>
<Button color="inherit" component={Link} to={'/'}>
My App
</Button>
</Typography>
<Button color="inherit" component={Link} to={'/register'}>
Register
</Button>
</Toolbar>
</AppBar>
</div>
)
}
const Register = (props) => {
const classes = useStyles()
const { setShowAlert, setAlertMessage, setAlertSeverity } = props
useEffect(() => {
console.log('use effected')
}, [])
const handleRegistration = () => {
setAlertMessage(
'You have succesfully registered! Please check your email for a verification link.'
)
setAlertSeverity('success')
setShowAlert(true)
}
return (
<Container maxWidth="sm" align="center" className={classes.root}>
This is some kind of form. I have redacted all input fields to
illustrate the issue more clearly.<br></br>
<Button
variant="contained"
color="primary"
onClick={handleRegistration}
>
Register
</Button>
</Container>
)
}
const MyAlert = (props) => {
const { severity, message } = props
return <Alert severity={severity}>{message}</Alert>
}
const App = () => {
const [alertSeverity, setAlertSeverity] = useState('')
const [alertMessage, setAlertMessage] = useState('')
const [showAlert, setShowAlert] = useState(false)
return (
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert ? (
<MyAlert severity={alertSeverity} message={alertMessage} />
) : (
''
)}
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
)
}
export default App
index.jsx
import * as React from "react"
import { render } from "react-dom"
import App from './App';
render(<App />, document.getElementById("app"))

Wouldn't that just be the case of using a setTimeout? When MyAlert is rendered, show it for 5 seconds and then dismiss.
First, pass it as a prop to your component (I also proposed a slightly different conditional render):
<BrowserRouter>
<div className="app">
<Navbar />
<div>{showAlert ? 'true' : 'false'}</div>
{showAlert &&
<MyAlert severity={alertSeverity} message={alertMessage} setShowAlert = {setShowAlert}/> }
<Switch>
<Route
path="/register"
render={(props) => (
<Register
{...props}
setShowAlert={setShowAlert}
setAlertMessage={setAlertMessage}
setAlertSeverity={setAlertSeverity}
/>
)}
/>
</Switch>
</div>
</BrowserRouter>
Then in your MyAlert component, use useEffect to trigger a timeout:
const MyAlert = (props) => {
useEffect(() => {
const timeout = setTimeout(() => {
props.setShowAlert(false); // Disable your alert after 5 seconds
}, 5000);
return () => {
clearTimeout(timeout); // Clears timer in case you close your alert somewhere else.
}
}, [])
}

Related

How to make a react global state re-render component?

I have tried using two different libraries to handle this issue and both time I get the same behavior.
Libraries tried: OvermindJS, Hookstate
When I click the button I can see the state change is beeing logged in the console but the component will only re-render on the second click
If I change page:
click Home
click Page1
click No Funds
Then it will show 1$
If I click straightway the No Funds button without changing page (first action on page) then the button will not re-render until it is clicked twice.
App.tsx
import * as React from "react"
import {
ChakraProvider,
Box,
Text,
Grid,
theme,
} from "#chakra-ui/react"
import { Switch, Route } from "wouter"
import { Appbar } from "./common/AppBar"
export const App = () => (
<ChakraProvider theme={theme}>
<Appbar />
<Box textAlign="center" fontSize="xl">
<Grid minH="100vh" p={3}>
<Switch>
<Route path="/">
<Text mt={100}>Home</Text>
</Route>
<Route path="/home">
<Text mt={100}>Home</Text>
</Route>
<Route path="/page1">
<Text mt={100}>Page 1</Text>
</Route>
</Switch>
</Grid>
</Box>
</ChakraProvider>
)
AppBar.tsx
import React, { ReactNode } from "react";
import {
Box,
Flex,
HStack,
Link,
IconButton,
Button,
Icon,
useDisclosure,
useColorModeValue,
Stack,
} from '#chakra-ui/react';
import { HamburgerIcon, CloseIcon } from '#chakra-ui/icons';
import { MdAccountBalanceWallet } from 'react-icons/md'
import { useLocation } from 'wouter';
import { actionIncrementFunds, globalState } from "../hookState/state";
import { useState } from "#hookstate/core";
const Links = ['Home', 'Page1'];
const NavLink: React.FC<any> = ({ children, handleClick }: { children: ReactNode, handleClick: any }) => (
<Link
px={2}
py={1}
rounded={'md'}
_hover={{
textDecoration: 'none',
bg: useColorModeValue('red.200', 'red.300'),
}}
onClick={() => handleClick(children)}>
{children}
</Link>
);
export const Appbar: React.FC = () => {
const state = useState(globalState);
const { isOpen, onOpen, onClose } = useDisclosure();
const [location, setLocation] = useLocation();
const handleClick = (path: string) => {
setLocation(`/${path.toLowerCase()}`)
}
const hasFunds = () => {
return state.currentFunds.get() > 0
}
const handleConnectWallet = () => {
actionIncrementFunds()
}
return (
<>
<Box zIndex={900} position={"fixed"} top={0} left={0} width="100%" bg={useColorModeValue('gray.100', 'gray.900')} px={4}>
<Flex h={16} alignItems={'center'} justifyContent={'space-between'}>
<IconButton
size={'md'}
icon={isOpen ? <CloseIcon /> : <HamburgerIcon />}
aria-label={'Open Menu'}
display={{ md: 'none' }}
onClick={isOpen ? onClose : onOpen}
/>
<HStack height={"100%"} spacing={8} alignItems={'center'}>
<HStack
as={'nav'}
spacing={4}
display={{ base: 'none', md: 'flex' }}>
{Links.map((link) => (
<NavLink handleClick={handleClick} key={link}>{link}</NavLink>
))}
</HStack>
</HStack>
<Flex alignItems={'center'}>
{hasFunds()
? <Button
variant={'solid'}
colorScheme={'red'}
size={'md'}
onClick={handleConnectWallet}
mr={4}>
{state.currentFunds.get()} $
</Button>
: <Button
variant={'solid'}
colorScheme={'red'}
size={'md'}
mr={4}
onClick={handleConnectWallet}
leftIcon={<Icon as={MdAccountBalanceWallet} />}>
No Funds
</Button>
}
</Flex>
</Flex>
{
isOpen ? (
<Box pb={4} display={{ md: 'none' }}>
<Stack as={'nav'} spacing={4}>
{Links.map((link) => (
<NavLink handleClick={handleClick} key={link}>{link}</NavLink>
))}
</Stack>
</Box>
) : null
}
</Box >
</>
);
}
Behavior example:
Repo with example for Hookstate:
https://github.com/crimson-med/state-issue
Repo with example for OvermindJS:
https://github.com/crimson-med/state-issue/tree/overmind
How can I get the button on change on the first click?

Unauthorized page restriction wants a refresh

I'm getting started with ReactJS and I wanted to restrict the sign-in page for not authorized users only and I want to redirect those users to the page they were before after they log in (that part is not working as well).
The problem is that when I log in and then go back to /sign-in page, it says that I'm still not authorized, even tho I can literally see the access token in local storage (look at the picture below). However, if I refresh the page, NotAuthorizedRoute works and it redirects me back to home page.
How can I fix this in React? How can I also redirect back to the previously opened page and not / only?
In other words, const isLoggedIn = localStorage.getItem('access_token') !== null; is being activated only after a page refresh. In angular, I used to fix such things by #Input/#Output and EventEmitter but I'm new to React and I don't know how to deal with it yet.
Image:
Login.js
import React, { Fragment, useState } from 'react';
import { useHistory } from 'react-router-dom';
import UserService from '../../Services/UserService';
import {
makeStyles,
Typography,
Container,
TextField,
FormControlLabel,
Checkbox,
Button,
Grid,
CircularProgress
} from '#material-ui/core';
const useStyles = makeStyles(theme => ({
content: {
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(8, 0, 6)
},
form: {
marginTop: theme.spacing(1)
},
submit: {
margin: theme.spacing(3, 0, 2)
}
}));
const Login = () => {
const classes = useStyles();
const history = useHistory();
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = (e) => {
e.preventDefault();
const user = {
username,
password,
};
setLoading(true);
UserService.loginUser(user)
.then(res => {
localStorage.setItem('access_token', res.token);
setLoading(false);
history.push('/');
})
.catch(err => {
setError(err.message);
setLoading(false);
});
}
return (
<Fragment>
<div className={classes.content}>
<Container maxWidth="sm">
<Typography component="h1" variant="h2" align="center" color="textPrimary" gutterBottom>
Sign In
</Typography>
<Typography variant="h5" align="center" color="textSecondary" paragraph>
If you are not registered, you should sign up.
</Typography>
</Container>
</div>
<Container maxWidth="md">
<Grid container justify="center" spacing={3}>
<Grid item xs={6}>
{!UserService.isLoggedIn ?
<form className={classes.form} noValidate onSubmit={handleSubmit}>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="text"
label="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
error={error !== ''}
helperText={error}
/>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
type="password"
label="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<FormControlLabel
control={<Checkbox value="remember" color="primary" />}
label="Remember me"
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
disabled={loading}
>
Sign In
</Button>
</form>
:
<Grid container justify="center">
Logged in.
</Grid>
}
{loading &&
<Grid container justify="center">
<CircularProgress className={classes.spinner} />
</Grid>
}
</Grid>
</Grid>
</Container>
</Fragment>
);
}
export default Login;
App.js
import React, { Fragment } from 'react';
import './App.css';
import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom';
import NotAuthorizedRoute from './Helpers/NotAuthorizedRoute';
import Navbar from './Components/Navbar/Navbar';
import Home from './Components/Home/Home';
import User from './Components/User/User';
import Login from './Components/Login/Login';
function App() {
return (
<Fragment>
<Router>
<Navbar />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/users" component={User} />
<NotAuthorizedRoute path="/sign-in" component={Login} />
<Redirect from="*" to="/" />
</Switch>
</Router>
</Fragment>
);
}
export default App;
Helpers/NotAuthorizedRoute.js
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
Service/UserService.js
const isLoggedIn = localStorage.getItem('access_token') !== null;
const loginUser = async (user) => {
const { username, password } = user;
if (username === 'qwe' && password === '123') {
return { token: 'access_token' };
} else {
throw new Error('Wrong username or password');
}
}
export default { isLoggedIn, loginUser };
EDIT: If I put !localStorage.getItem('access_token') instead of !UserService.isLoggedIn in NotAuthorizedRoute, it works. Why?
In React, the component only re-renders when props or state changes. In your NotAuthorizedRoute you're directly using isLoggedIn param from file. Instead, you should pass isLoggedIn as prop from parent component. So you can re-write NotAuthorizedRoute.js as:
import React from 'react';
import UserService from '../Services/UserService';
import { Redirect, Route } from 'react-router-dom';
const NotAuthorizedRoute = ({ component: Component, isLoggedIn=false, ...rest }) => {
return (
<Route
{...rest}
render={props =>
!UserService.isLoggedIn ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: '/login', state: { from: props.location } }} />
)
}
/>
)
}
export default NotAuthorizedRoute;
and call this component as
<NotAuthorizedRoute ... isLoggedIn={UserService.isLoggedIn} />

React Dialog box closed issue

I want to use a dialog box of material UI. I am navigating that dialog box from the right side menu(sidebars -> user registration), using another component. I just want to open that dialog box on the current page home or about us. It is a user registration dialog. Can you help on that?
When I tried to open the user registration dialog, I am unable to open the dialog box in the current page, that's why I have created a separate component for the dialog box component.
I want to open that dialog when I select the side menu option. The dialog box should be open in the current page.
This is the code sandbox link. https://codesandbox.io/s/immutable-sound-ggj4w
App js
import React from "react";
import "./styles.css";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import Home from "./home";
import About from "./about";
import Dialog from "./dialog";
import SideMenu from "./sidemu";
export default function App() {
return (
<div className="App">
<BrowserRouter>
<SideMenu />
{/* <Switch> */}
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
<Route exact path="/sidemenu" component={SideMenu} />
<Route exact path="/dialog" component={Dialog} />
{/* </Switch> */}
</BrowserRouter>
{/* <h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2> */}
</div>
);
}
home js
import React, { Component } from "react";
class Home extends Component {
render() {
return (
<div>
<div>Home</div>
</div>
);
}
}
export default Home;
about us
import React, { Component } from "react";
class Home extends Component {
render() {
return (
<div>
<div>about us</div>
</div>
);
}
}
export default Home;
dialog js
import React from 'react';
import Button from '#material-ui/core/Button';
import Dialog from '#material-ui/core/Dialog';
import DialogActions from '#material-ui/core/DialogActions';
import DialogContent from '#material-ui/core/DialogContent';
import DialogContentText from '#material-ui/core/DialogContentText';
import DialogTitle from '#material-ui/core/DialogTitle';
export default function AlertDialog() {
const [open, setOpen] = React.useState(false);
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<div>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
Open alert dialog
</Button>
<Dialog
open={true}
onClose={handleClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">{"Use Google's location service?"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Let Google help apps determine location. This means sending anonymous location data to
Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={handleClose} color="primary">
Disagree
</Button>
<Button onClick={handleClose} color="primary" autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
</div>
);
}
sidemenu js
import React from 'react';
import './styles.css';
import { Link } from 'react-router-dom';
import { makeStyles } from '#material-ui/core/styles';
import Drawer from '#material-ui/core/Drawer';
import { List, ListItemIcon, ListItem, ListItemText } from '#material-ui/core';
import MenuRoundedIcon from '#material-ui/icons/MenuRounded';
import HomeRoundedIcon from '#material-ui/icons/HomeRounded';
import MenuBookRoundedIcon from '#material-ui/icons/MenuBookRounded';
const useStyles = makeStyles({
list: {
width: 250,
},
fullList: {
width: 'auto',
},
});
export default function TemporaryDrawer() {
const classes = useStyles();
const [state, setState] = React.useState({
top: false,
left: false,
bottom: false,
right: false,
});
const toggleDrawer = (side, open) => event => {
if (event.type === 'keydown' && (event.key === 'Tab' || event.key === 'Shift')) {
return;
}
setState({ ...state, [side]: open });
};
const sideList = side => (
<div
className={classes.list}
role="presentation"
onClick={toggleDrawer(side, false)}
onKeyDown={toggleDrawer(side, false)}
>
<List>
<Link className="right-menu-data" to="/">
<ListItem button>
<ListItemIcon>
<HomeRoundedIcon />
</ListItemIcon>
<ListItemText>Home</ListItemText>
</ListItem>
</Link>
<Link className="right-menu-data" to="/about"><ListItem button>
<ListItemIcon>
<MenuBookRoundedIcon />
</ListItemIcon>
<ListItemText>About us</ListItemText>
</ListItem>
</Link>
<Link className="right-menu-data" to="/dialog"><ListItem button>
<ListItemIcon>
<MenuBookRoundedIcon />
</ListItemIcon>
<ListItemText>User Registration</ListItemText>
</ListItem>
</Link>
</List>
</div>
);
return (
<div>
<MenuRoundedIcon className="careerpedia-menu-bars" onClick={toggleDrawer('right', true)} />
<Drawer anchor="right" open={state.right} onClose={toggleDrawer('right', false)}>
{sideList('right')}
</Drawer>
</div>
);
}
One way to achieving this is to factor the logic to open/close the modal out of the dialog component to a "central" location, preferably in the app, but outside the router. This allows the dialog to be opened/closed from a single location by callbacks that can be passed around the app, and not be coupled to any specific route your app is on.
A quick refactoring of your code:
dialog.js
const AlertDialog = ({ open, onAgree, onClose, onDisagree }) => (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
{"Use Google's location service?"}
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Let Google help apps determine location. This means sending anonymous
location data to Google, even when no apps are running.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onDisagree} color="primary">
Disagree
</Button>
<Button onClick={onAgree} color="primary" autoFocus>
Agree
</Button>
</DialogActions>
</Dialog>
);
App.js
export default function App() {
const [open, setOpen] = useState(false);
const openDialog = () => setOpen(true);
const closeDialog = () => setOpen(false);
const onAgreeHandler = () => {
closeDialog();
alert("AGREED");
};
const onDisgreeHandler = () => {
closeDialog();
alert("DISAGREED");
};
const onCloseHandler = (event, reason) => {
closeDialog();
alert(`Dialog closed. Reason: ${reason}`);
};
return (
<div className="App">
<BrowserRouter>
<SideMenu openDialog={openDialog} /> // Pass dialog open callback to sidemenu
<Route exact path="/" component={Home} />
<Route exact path="/about" component={About} />
<Route exact path="/dialog" component={Dialog} />
</BrowserRouter>
<Button variant="outlined" color="primary" onClick={openDialog}>
Open alert dialog test
</Button>
<Dialog
open={open}
onAgree={onAgreeHandler}
onClose={onCloseHandler}
onDisagree={onDisgreeHandler}
/>
</div>
);
}
sidemu.js
export default function TemporaryDrawer({ openDialog }) { // Accept openDialog callback
...
{ /* Add new menu list item (not in a Link!) */ }
<ListItem button onClick={openDialog}> // Set onClick handler to openDialog callback
<ListItemIcon>
<MenuBookRoundedIcon />
</ListItemIcon>
<ListItemText>Trigger dialog?</ListItemText>
</ListItem>
NOTE: As your app grows in size/complexity this pattern may become a little unwieldy (if you need to open a dialog by any further descendants) and you'll likely want to switch to using a react context provider/consumer specifically for the dialog, or use a state management system like Redux so you can dispatch simple actions to open/close it.

Web app taking too long to determine whether user is authenticated or not

Background info
I have just started using/ learning how to use Next.js and I'm facing an issue where my user authentication logic is making my components take a long time to render on the page. I believe I'm missing something really fundamental and I'm not sure if it's Next.js related or something to do with how I'm handling user states across my application.
I'm using Firebase's Google Authentication to handle my user login.
The code I will be referring to in my question exists in the following repository:
https://github.com/myamazingthrowaway/nextjswebsite
A live demo of the app can be found here:
https://nextjswebsite-kappa-sand.now.sh/
(it uses cross-site cookies to handle firebase google login - I don't know how to change this default behaviour, so if it doesn't work first time, make sure your browser allows cross-site cookies)
I based my authentication logic on the following repository:
https://github.com/taming-the-state-in-react/nextjs-redux-firebase-authentication
My web app was made with create-next-app.
The Problem
When a user visits my website, the sidebar component and the rest of the components which rely on the logged in state of the user don't appear immediately on page load. They appear some time after the initial page is rendered. It's a significant delay and there is no 'loading indicator' on my chrome tab to say that the dom is still being built Is this the expected behaviour?
The issue can also be seen on the following site (signing in with google demonstrates what I mean).
Steps to reproduce:
1. Go to: https://kage.saltycrane.com/
2. Press 'Sign In'
3. Press 'Sign In with Google'
You will get redirected to the google sign in page, select an account etc. (to sign in)
Then you will be redirected back to the site in step 1, where the menu bar at the top still remains 'Sign in'.. for a moment or two, before it changes to your email address.
Why does this happen?
(the code behind the webpage above is here: https://github.com/saltycrane/kage)
My Code
In my _app.js file, I have a 'Shell' component which handles my sidebar & navbar for the entire web app. It accepts child components to be rendered within the confines of the sidebar etc. Perhaps this isn't the best way to handle how the application flows (would be more than happy for suggestions on how to improve this).
The _app.js file looks like this:
import React from "react";
import App from "next/app";
import CssBaseline from "#material-ui/core/CssBaseline";
import { ThemeProvider } from "#material-ui/styles";
import { Provider } from "react-redux";
import withRedux from "next-redux-wrapper";
import initStore from "../src/store";
import theme from "../src/theme";
import Shell from "../src/components/Shell";
class EnhancedApp extends App {
static async getInitialProps({ Component, ctx }) {
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {}
};
}
componentDidMount() {
const jssStyles = document.querySelector("#jss-server-side");
if (jssStyles) {
jssStyles.parentNode.removeChild(jssStyles);
}
}
render() {
const { Component, pageProps, store } = this.props;
return (
<>
<Provider store={store}>
<ThemeProvider theme={theme}>
<title>Next.js</title>
<CssBaseline />
<Shell>
<Component {...pageProps} />
</Shell>
</ThemeProvider>
</Provider>
</>
);
}
}
export default withRedux(initStore)(EnhancedApp);
My Shell component looks like this:
import React from "react";
import Router from "next/router";
import { connect } from "react-redux";
import {
Drawer,
List,
Divider,
ListItem,
ListItemIcon,
ListItemText,
Hidden,
AppBar,
Toolbar,
IconButton,
Button
} from "#material-ui/core";
import { ProfileIcon } from "../index";
import MonetizationOnOutlinedIcon from "#material-ui/icons/MonetizationOnOutlined";
import AccountBalanceWalletRoundedIcon from "#material-ui/icons/AccountBalanceWalletRounded";
import AccountBoxRoundedIcon from "#material-ui/icons/AccountBoxRounded";
import VpnKeyRoundedIcon from "#material-ui/icons/VpnKeyRounded";
import ExitToAppRoundedIcon from "#material-ui/icons/ExitToAppRounded";
import MenuIcon from "#material-ui/icons/Menu";
import { makeStyles } from "#material-ui/core/styles";
import * as routes from "../../constants/routes";
import { auth } from "../../firebase/firebase";
const drawerWidth = 180;
const useStyles = makeStyles(theme => ({
content: {
flexGrow: 1,
padding: theme.spacing(3)
},
root: {
display: "flex"
},
container: {
flexGrow: 1
},
toolbar: theme.mixins.toolbar,
drawer: {
[theme.breakpoints.up("md")]: {
width: drawerWidth,
flexShrink: 0
}
},
drawerPaper: {
width: drawerWidth
},
appBar: {
background: "linear-gradient(45deg, #FF8E53 30%, #ff4d73 90%)",
marginLeft: drawerWidth,
[theme.breakpoints.up("md")]: {
width: `calc(100% - ${drawerWidth}px)`
}
},
logoContainer: {
background: "linear-gradient(45deg, #ff4d73 30%, #FF8E53 90%)",
justifyContent: "center",
flexDirection: "column",
height: "15rem"
},
menuButton: {
marginRight: theme.spacing(2),
[theme.breakpoints.up("md")]: {
display: "none"
}
},
rightAlign: {
marginLeft: "auto",
marginRight: -12,
cursor: "pointer"
},
hoverCursor: {
cursor: "pointer"
}
}));
const Shell = ({ children, authUser }) => {
const classes = useStyles();
const [mobileOpen, setMobileOpen] = React.useState(false);
const handleGoToEarnPage = () => {
Router.push(routes.EARN);
if (mobileOpen) handleDrawerToggle();
};
const handleGoToSignInPage = () => {
Router.push(routes.SIGN_IN);
if (mobileOpen) handleDrawerToggle();
};
const handleGoToWithdrawPage = () => {
Router.push(routes.WITHDRAW);
if (mobileOpen) handleDrawerToggle();
};
const handleGoToProfilePage = () => {
Router.push(routes.PROFILE);
if (mobileOpen) handleDrawerToggle();
};
const handleDrawerToggle = () => {
setMobileOpen(!mobileOpen);
};
const handleGoToHomePage = () => {
Router.push(routes.LANDING);
if (mobileOpen) handleDrawerToggle();
};
const handleSignOut = () => {
auth.signOut();
if (mobileOpen) handleDrawerToggle();
};
const drawer = (
<>
<AppBar position="static">
<Toolbar className={classes.logoContainer}>
<img
src="/images/logo/logo.png"
alt="my logo"
height="120rem"
onClick={handleGoToHomePage}
className={classes.hoverCursor}
/>
</Toolbar>
</AppBar>
<List>
<ListItem button key="Earn" href="/earn" onClick={handleGoToEarnPage}>
<ListItemIcon>
<MonetizationOnOutlinedIcon />
</ListItemIcon>
<ListItemText primary="Earn" />
</ListItem>
<ListItem
button
key="Withdraw"
href="/withdraw"
onClick={handleGoToWithdrawPage}
>
<ListItemIcon>
<AccountBalanceWalletRoundedIcon />
</ListItemIcon>
<ListItemText primary="Withdraw" />
</ListItem>
<Divider variant="middle" />
{!authUser && (
<List>
<ListItem
button
key="Sign In"
href="/signin"
onClick={handleGoToSignInPage}
>
<ListItemIcon>
<VpnKeyRoundedIcon />
</ListItemIcon>
<ListItemText primary="Sign In" />
</ListItem>
</List>
)}
{authUser && (
<List>
<ListItem
button
key="Profile"
href="/profile"
onClick={handleGoToProfilePage}
>
<ListItemIcon>
<AccountBoxRoundedIcon />
</ListItemIcon>
<ListItemText primary="Profile" />
</ListItem>
<ListItem button key="Sign Out" onClick={handleSignOut}>
<ListItemIcon>
<ExitToAppRoundedIcon />
</ListItemIcon>
<ListItemText primary="Sign Out" />
</ListItem>
</List>
)}
</List>
</>
);
return (
<div className={classes.root}>
<AppBar position="fixed" className={classes.appBar}>
<Toolbar>
<IconButton
color="inherit"
aria-label="open drawer"
edge="start"
onClick={handleDrawerToggle}
className={classes.menuButton}
>
<MenuIcon />
</IconButton>
<div className={classes.rightAlign}>
{authUser && <ProfileIcon className={classes.hoverCursor} />}
{!authUser && (
<Button color="inherit" onClick={handleGoToSignInPage}>
Sign In
</Button>
)}
</div>
</Toolbar>
</AppBar>
<nav className={classes.drawer} aria-label="sidebar">
<Hidden mdUp>
<Drawer
variant="temporary"
anchor={classes.direction === "rtl" ? "right" : "left"}
open={mobileOpen}
onClose={handleDrawerToggle}
classes={{
paper: classes.drawerPaper
}}
ModalProps={{
keepMounted: true // Better open performance on mobile.
}}
>
{drawer}
</Drawer>
</Hidden>
<Hidden smDown>
<Drawer
classes={{
paper: classes.drawerPaper
}}
variant="permanent"
open
>
{drawer}
</Drawer>
</Hidden>
</nav>
<main className={classes.content}>
<div className={classes.toolbar} />
{children}
</main>
</div>
);
};
const mapStateToProps = state => ({
authUser: state.sessionState.authUser
});
export default connect(mapStateToProps)(Shell);
As you can see, the Shell component uses a HOC to wrap it with an authUser prop from the session state. I don't know if this is what causes issues when loading the page?
The ProfileIcon component doesn't load immediately when user signs in either. Similarly to the kage website I mentioned earlier. I don't understand why this happens. I feel like my code is all over the place.
My signin.js page looks like this:
import React from "react";
import Router from "next/router";
import Button from "#material-ui/core/Button";
import { AppWithAuthentication } from "../src/components/App";
import { auth, provider } from "../src/firebase/firebase";
import { db } from "../src/firebase";
import * as routes from "../src/constants/routes";
const SignInPage = () => (
<AppWithAuthentication>
<h1>Sign In</h1>
<SignInForm />
</AppWithAuthentication>
);
const updateByPropertyName = (propertyName, value) => () => ({
[propertyName]: value
});
const INITIAL_STATE = {
user: null,
error: null
};
class SignInForm extends React.Component {
constructor(props) {
super(props);
this.state = { ...INITIAL_STATE };
if (auth.currentUser) {
console.log(`already signed in`);
Router.push(routes.HOME);
}
}
componentDidMount() {
auth.onAuthStateChanged(user => {
if (user) {
console.log(user);
// add them to the db and then redirect
db.doCreateUser(
user.uid,
user.email,
user.displayName,
user.photoURL,
false
)
.then(() => {
this.setState(() => ({ ...INITIAL_STATE }));
Router.push(routes.HOME);
})
.catch(error => {
this.setState(updateByPropertyName("error", error));
});
} else {
console.log(`No active user found. User must log in`);
}
});
}
onClick = () => {
auth.signInWithRedirect(provider);
};
render() {
return (
<Button variant="contained" color="primary" onClick={this.onClick}>
Sign In with Google
</Button>
);
}
}
export default SignInPage;
export { SignInForm };
Where AppWithAuthentication looks like this:
import React from "react";
import { compose } from "recompose";
import withAuthentication from "../Session/withAuthentication";
import withAuthorisation from "../Session/withAuthorisation";
const App = ({ children }) => (
<div className="app">
{children}
</div>
);
const AppWithAuthentication = compose(
withAuthentication,
withAuthorisation(false)
)(App);
const AppWithAuthorisation = compose(
withAuthentication,
withAuthorisation(true)
)(App);
export { AppWithAuthentication, AppWithAuthorisation };
So whenever a user goes onto my webpage and tries to access any 'authenticated only' route, they will see the content of the route first for a few seconds, then get redirected to the sign in page. I don't want this to happen and I cannot see why this happens either.
How can I fix these issues? I'm completely stuck for ideas. Need a fresh pair of eyes to help me understand where the issue is.
This is caused by the request of the authentication process waiting on a response.
Check your network connections in chrome with developer tools (Ctrl+Shift+I) -> Network -> Then reload your app and see the requests firing. You will notice your state will wait on https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyAssertion?key=xxx to return a response with the google account name etc. after you click sign in and select an account.
Make the loaders visibility dependant on the information the request returns (create a variable in your state (exp.: user) and once you have the user info from google set it to the user object. Then you can say !user -> show loader. You can also trigger the loader with an additional state variable that gets set true when you click on sign in.
Considering you are using firebase, look into https://www.robinwieruch.de/complete-firebase-authentication-react-tutorial for some more info. I would include code here as well, but I think you can use this information to build what I said according to how you want to structure your app. Shouldn't be too big of a deal. Hope it helps!
Remove this part
static async getInitialProps({ Component, ctx }) {
return {
pageProps: Component.getInitialProps
? await Component.getInitialProps(ctx)
: {}
};
}
Let nextjs do the optimization it self.
when you handle the initial props yourself, the optimization is lost.

Material-UI's Tabs integration with react router 4?

The new react-router syntax uses the Link component to move around the routes. But how could this be integrated with material-ui?
In my case, I'm using tabs as the main navigation system, So in theory I should have something like this:
const TabLink = ({ onClick, href, isActive, label }) =>
<Tab
label={label}
onActive={onClick}
/>
export default class NavBar extends React.Component {
render () {
return (
<Tabs>
<Link to="/">{params => <TabLink label="Home" {...params}/>}</Link>
<Link to="/shop">{params => <TabLink label="shop" {...params}/>}</Link>
<Link to="/gallery">{params => <TabLink label="gallery" {...params}/>}</Link>
</Tabs>
)
}
}
But when it renders, material-ui throws an error that the child of Tabs must be a Tab component. What could be the way to proceed? How do I manage the isActive prop for the tab?
Thanks in advance
Another solution (https://codesandbox.io/s/l4yo482pll) with no handlers nor HOCs, just pure react-router and material-ui components:
import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import Tabs from "#material-ui/core/Tabs";
import Tab from "#material-ui/core/Tab";
import { Switch, Route, Link, BrowserRouter, Redirect } from "react-router-dom";
function App() {
const allTabs = ['/', '/tab2', '/tab3'];
return (
<BrowserRouter>
<div className="App">
<Route
path="/"
render={({ location }) => (
<Fragment>
<Tabs value={location.pathname}>
<Tab label="Item One" value="/" component={Link} to={allTabs[0]} />
<Tab label="Item Two" value="/tab2" component={Link} to={allTabs[1]} />
<Tab
value="/tab3"
label="Item Three"
component={Link}
to={allTabs[2]}
/>
</Tabs>
<Switch>
<Route path={allTabs[1]} render={() => <div>Tab 2</div>} />
<Route path={allTabs[2]} render={() => <div>Tab 3</div>} />
<Route path={allTabs[0]} render={() => <div>Tab 1</div>} />
</Switch>
</Fragment>
)}
/>
</div>
</BrowserRouter>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
My instructor helped me with using React Router 4.0's withRouter to wrap the Tabs component to enable history methods like so:
import React, {Component} from "react";
import {Tabs, Tab} from 'material-ui';
import { withRouter } from "react-router-dom";
import Home from "./Home";
import Portfolio from "./Portfolio";
class NavTabs extends Component {
handleCallToRouter = (value) => {
this.props.history.push(value);
}
render () {
return (
<Tabs
value={this.props.history.location.pathname}
onChange={this.handleCallToRouter}
>
<Tab
label="Home"
value="/"
>
<div>
<Home />
</div>
</Tab>
<Tab
label="Portfolio"
value="/portfolio"
>
<div>
<Portfolio />
</div>
</Tab>
</Tabs>
)
}
}
export default withRouter(NavTabs)
Simply add BrowserRouter to index.js and you're good to go.
The error you are seeing from material-ui is because it expects to have a <Tab> component rendered as direct child of <Tabs> component.
Now, here is a way that I've found to integrate the link into the <Tabs> component without loosing the styles:
import React, {Component} from 'react';
import {Tabs, Tab} from 'material-ui/Tabs';
import {Link} from 'react-router-dom';
export default class MyComponent extends Component {
render() {
const {location} = this.props;
const {pathname} = location;
return (
<Tabs value={pathname}>
<Tab label="First tab" containerElement={<Link to="/my-firs-tab-view" />} value="/my-firs-tab-view">
{/* insert your component to be rendered inside the tab here */}
</Tab>
<Tab label="Second tab" containerElement={<Link to="/my-second-tab-view" />} value="/my-second-tab-view">
{/* insert your component to be rendered inside the tab here */}
</Tab>
</Tabs>
);
}
}
To manage the 'active' property for the tabs, you can use the value property in the <Tabs> component and you also need to have a value property for each tab, so when both of the properties match, it will apply the active style to that tab.
Solution with Tab highlight, Typescript based and works well with react-route v5:
Explanation: <Tab/> here work as a link to React router. Values in <Tab/> to={'/all-event'} and value={'/all-event'} should be same in order to highlgiht
import { Container, makeStyles, Tab, Tabs } from '#material-ui/core';
import React from 'react';
import {
Link,
Route,
Switch,
useLocation,
Redirect,
} from 'react-router-dom';
import AllEvents from './components/AllEvents';
import UserEventsDataTable from './components/UserEventsDataTable';
const useStyles = makeStyles(() => ({
container: {
display: 'flex',
justifyContent: 'center',
},
}));
function App() {
const classes = useStyles();
const location = useLocation();
return (
<>
<Container className={classes.container}>
<Tabs value={location.pathname}>
<Tab
label='All Event'
component={Link}
to={`/all-event`}
value={`/all-event`}
/>
<Tab
label='User Event'
component={Link}
to={`/user-event`}
value={`/user-event`}
/>
</Tabs>
</Container>
<Switch>
<Route path={`/all-event`}>
<AllEvents />
</Route>
<Route path={`/user-event`}>
<UserEventsDataTable />
</Route>
<Route path={`/`}>
<Redirect from='/' to='/all-event' />
</Route>
</Switch>
</>
);
}
export default App;
Here's another solution, using the beta of Material 1.0 and adding browser Back/Forward to the mix:
import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from 'material-ui/styles';
import AppBar from 'material-ui/AppBar';
import Tabs, { Tab } from 'material-ui/Tabs';
import { withRouter } from "react-router-dom";
import Home from "./Home";
import Portfolio from "./Portfolio";
function TabContainer(props) {
return <div style={{ padding: 20 }}>{props.children}</div>;
}
const styles = theme => ({
root: {
flexGrow: 1,
width: '100%',
marginTop: theme.spacing.unit * 3,
backgroundColor: theme.palette.background.paper,
},
});
class NavTabs extends React.Component {
state = {
value: "/",
};
componentDidMount() {
window.onpopstate = ()=> {
this.setState({
value: this.props.history.location.pathname
});
}
}
handleChange = (event, value) => {
this.setState({ value });
this.props.history.push(value);
};
render() {
const { classes } = this.props;
const { value } = this.state;
return (
<div className={classes.root}>
<AppBar position="static" color="default">
<Tabs
value={value}
onChange={this.handleChange}
scrollable
scrollButtons="on"
indicatorColor="primary"
textColor="primary"
>
<Tab label="Home" value = "/" />
<Tab label="Portfolio" value = "/portfolio"/>
</Tabs>
</AppBar>
{value === "/" && <TabContainer>{<Home />}</TabContainer>}
{value === "/portfolio" && <TabContainer>{<Portfolio />}</TabContainer>}
</div>
);
}
}
NavTabs.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withRouter(withStyles(styles)(NavTabs));
You can use browserHistory instead of React-Router Link component
import { browserHistory } from 'react-router'
// Go to /some/path.
onClick(label) {
browserHistory.push('/${label}');
}
// Example for Go back
//browserHistory.goBack()
<Tabs>
<Tab
label={label}
onActive={() => onClick(label)}
/>
</Tabs>
As you see you can simply push() your target to the browserHistory
As #gkatchmar says you can use withRouter high-order component but you can also use context API. Since #gkatchmar showed withRouter already I will only show context API. Bear in mind that this is an experimental API.
https://stackoverflow.com/a/42716055/3850405
import React, {Component} from "react";
import {Tabs, Tab} from 'material-ui';
import * as PropTypes from "prop-types";
export class NavTabs extends Component {
constructor(props) {
super(props);
}
static contextTypes = {
router: PropTypes.object
}
handleChange = (event: any , value: any) => {
this.context.router.history.push(value);
};
render () {
return (
<Tabs
value={this.context.router.history.location.pathname}
onChange={this.handleChange}
>
<Tab
label="Home"
value="/"
>
<div>
<Home />
</div>
</Tab>
<Tab
label="Portfolio"
value="/portfolio"
>
<div>
<Portfolio />
</div>
</Tab>
</Tabs>
)
}
}
Here's a simple solution using the useLocation hook. No state needed. React router v5 though.
import { Tab, Tabs } from '#material-ui/core';
import { matchPath, NavLink, useLocation } from 'react-router-dom';
const navItems = [
{
id: 'one',
path: '/one',
text: 'One',
},
{
id: 'two',
path: '/two',
text: 'Two',
},
{
id: 'three',
path: '/three',
text: 'Three',
},
];
export default function Navigation() {
const { pathname } = useLocation();
const activeItem = navItems.find((item) => !!matchPath(pathname, { path: item.path }));
return (
<Tabs value={activeItem?.id}>
{navItems.map((item) => (
<Tab key={item.id} value={item.id} label={item.text} component={NavLink} to={item.path} />
))}
</Tabs>
);
}
<BrowserRouter>
<div className={classes.root}>
<AppBar position="static" color="default">
<Tabs
value={this.state.value}
onChange={this.handleChange}
indicatorColor="primary"
textColor="primary"
fullWidth
>
<Tab label="Item One" component={Link} to="/one" />
<Tab label="Item Two" component={Link} to="/two" />
</Tabs>
</AppBar>
<Switch>
<Route path="/one" component={PageShell(ItemOne)} />
<Route path="/two" component={PageShell(ItemTwo)} />
</Switch>
</div>
I've created this hook to help control the tabs and generate the default value that catches from the location URL.
const useTabValue = (array, mainPath = "/") => {
const history = useHistory();
const { pathname } = useLocation();
const [value, setValue] = useState(0);
const pathArray = pathname.split("/");
function handleChange(_, nextEvent) {
setValue(nextEvent);
history.push(`${mainPath}/${array[nextEvent]}`);
}
const findDefaultValue = useCallback(() => {
return array.forEach((el) => {
if (pathArray.indexOf(el) > 0) {
setValue(array.indexOf(el));
return;
}
});
}, [pathArray, array]);
useEffect(() => {
findDefaultValue();
}, [findDefaultValue]);
return {
handleChange,
value,
};
};
then I have used it like this :
const NavigationBar = () => {
const classes = useStyles();
const allTabs = useMemo(() => ["home", "search"]);
const { handleChange, value } = useTabValue(allTabs, "/dashboard");
return (
<div className={classes.navBarContainer}>
<Tabs
centered
value={value}
variant="fullWidth"
onChange={handleChange}
className={classes.navBar}
>
<Tab color="textPrimary" icon={<HomeIcon />} />
<Tab color="textPrimary" icon={<ExploreIcon />} />
</Tabs>
</div>
);
};
I solved this in a much easier fashion (I was surprised this worked so well - maybe there's a problem I haven't found out). I'm using Router 6 and React 17 (I know these packages are newer).
In any case, I just used the useNavigate hook in the handleChange function. Thus, now there is NO need for Switch and the code becomes much simpler. See below:
let navigate = useNavigate();
const [selection, setSelection] = useState();
const handleChange = (event, newValue) => {
setSelection(newValue);
navigate(`${newValue}`);
}
return (
<Tabs value={selection} onChange={handleChange}>
<Tab label="Products" value="products" />
<Tab label="Customers" value="customers" />
<Tab label="Invoices" value="invoices" />
</Tabs>
);
}
The handleChange function updates 'selection' which controls the display of the tabs, and also navigates to the right path.
if you set the component somewhere in your React space, and set correctly a :style route (as explained by React Router: https://reactrouter.com/docs/en/v6/getting-started/overview), you can also control in which area of the page will the content be rendered. Hope it helps somebody!
I got it working this way in my app:
import React, {useEffect, useRef} from 'react';
import PropTypes from 'prop-types';
import {makeStyles} from '#material-ui/core/styles';
import AppBar from '#material-ui/core/AppBar';
import Tabs from '#material-ui/core/Tabs';
import Tab from '#material-ui/core/Tab';
import Typography from '#material-ui/core/Typography';
import Box from '#material-ui/core/Box';
import Container from "#material-ui/core/Container";
import {Link} from "react-router-dom";
import MenuIcon from "#material-ui/icons/Menu";
import VideoCallIcon from "#material-ui/icons/VideoCall";
const docStyles = makeStyles(theme => ({
root: {
display: 'flex',
'& > * + *': {
marginLeft: theme.spacing(2),
},
},
appBarRoot: {
flexGrow: 1,
},
headline: {
marginTop: theme.spacing(2),
},
bodyCopy: {
marginTop: theme.spacing(1),
fontSize: '1.2rem',
},
tabContents: {
margin: theme.spacing(3),
},
}));
function TabPanel(props) {
const {children, value, index, classes, ...other} = props;
return (
<div
role="tabpanel"
hidden={value !== index}
id={`simple-tabpanel-${index}`}
aria-labelledby={`simple-tab-${index}`}
{...other}
>
{value === index && (
<Container>
<Box className={classes.tabContents}>
{children}
</Box>
</Container>
)}
</div>
);
}
function a11yProps(index) {
return {
id: `simple-tab-${index}`,
'aria-controls': `simple-tabpanel-${index}`,
};
}
function TabOneContents(props) {
const {classes} = props;
return (
<>
<Typography variant="h4" component={'h1'} className={classes.headline}>
Headline 1
</Typography>
<Typography variant="body1" className={classes.bodyCopy}>
Body Copy 1
</Typography>
</>
)
}
function TabTwoContents(props) {
const {classes} = props;
const nurseOnboardingPath = '/navigator/onboarding/' + Meteor.userId() + '/1';
return (
<>
<Typography variant="h4" component={'h1'} className={classes.headline}>
Headline 2
</Typography>
<Typography variant="body1" className={classes.bodyCopy}>
Body Copy 2
</Typography>
</>
)
}
export default function MUITabPlusReactRouterDemo(props) {
const {history, match} = props;
const propsForDynamicClasses = {};
const classes = docStyles(propsForDynamicClasses);
const [value, setValue] = React.useState(history.location.pathname.includes('/tab_2') ? 1 : 0);
const handleChange = (event, newValue) => {
setValue(newValue);
const pathName = '/' + (value == 0 ? 'tab_1' : 'tab_2');
history.push(pathName);
};
return (
<div className={classes.appBarRoot}>
<AppBar position="static" color="transparent">
<Tabs value={value} onChange={handleChange} aria-label="How It Works" textColor="primary">
<Tab label="Tab 1" {...a11yProps(0)} />
<Tab label="Tab 2" {...a11yProps(1)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0} classes={classes}>
<TabOneContents classes={classes}/>
</TabPanel>
<TabPanel value={value} index={1} classes={classes}>
<TabTwoContents classes={classes}/>
</TabPanel>
</div>
);
}
...and in React Router:
[.....]
<Route exact path="/tab_1"
render={(routeProps) =>
<MUITabPlusReactRouterDemo history={routeProps.history}
/>
}/>
<Route exact path="/tab_2"
render={(routeProps) =>
<MUITabPlusReactRouterDemo history={routeProps.history} />
}/>
[.....]

Resources