Background info
I have just started using/ learning how to use Next.js and developed a basic application which allows users to view several pages, some of which are authentication protected. Users must sign in/ up, in order to view content on other pages.
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
What I'd like to achieve
I'd like to know how to improve my logic and code, to utilise the power of next.js and server side rendering. I'd like to make sure I'm using appropriate, safe & efficient code for my user authentication. Most importantly, I'm here to learn from my mistakes and expand my knowledge of user authentication, higher order components and the next.js workflow.
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 and would be more than happy to accept 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.
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 website and tries to access any 'authenticated only' route, they will get redirected to the sign in page. Additionally, when a signed in user visits the sign in page, they will automatically get redirected to the home
page.
Difficulties I'm facing
After a user logs in, it takes a little while for the 'authentication protected' content to load on the page. It seems like the page loads, then the app waits for the session state to be updated, then it renders the appropriate content (in the sidebar and appbar - part of the Shell
). I'm not sure if this is expected behaviour but I'd like to understand why this happens and whether I can do anything about it?