6
\$\begingroup\$

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?

\$\endgroup\$
2
  • \$\begingroup\$ I didn't take a full look at your work but I noticed a big no no in your react app, you are using react hooks in a component and a class-based component in other places and that's anti-pattern cause why use hooks if you still use class-based components, react hooks are here to replace and eliminate the need for class-based components because functional components are more efficient the class-based components. \$\endgroup\$ Commented Jan 12, 2020 at 21:35
  • \$\begingroup\$ @ChamsddineBouzaine Thanks for the comment. This suggests I should prioritise on converting all of my existing codebase to utilise react hooks first before continuing with anything else? \$\endgroup\$ Commented Jan 13, 2020 at 8:58

1 Answer 1

4
+25
\$\begingroup\$
  • stop using recompose because it got deprecated and you don't need it if you use react hooks.
  • transform your components and use react hooks because it's an anti-pattern to use them with class components.
  • Try to decompose your components more like for the shell components it's a heavy component try to extract for say the drawer component to its own file Drawer.JS then use it in Shell component also try to extract the style object that you pass to the makeStyles function for material-UI this all to make your code more maintainable.
  • try to avoid repeating your self like in the shell component try to optimize your algorithm.
  • separate the view logic from the service logic like for the sign in components componentDidMount() function you have to separate that two logic and create a new level of abstraction "basically a function that handles the DB logic " and just execute it there.
  • now for the last question the difficulty you are having I don't quite understand it, I have reviewed your demo and i didn't encounter a problem after I signed in.
\$\endgroup\$
2
  • \$\begingroup\$ Thanks for your reply. As for the last point, The issue can also be seen on the following site (signing in with google demonstrates what I mean). Steps to reproduce: 1. Go to: 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.The same thing happens to my web app \$\endgroup\$ Commented Jan 14, 2020 at 8:32
  • \$\begingroup\$ As for my web app, the issue still persists. I'm not sure how you managed to miss it. The icon that replaces the 'Sign in' at the top right does not get updated until at least 2-4 seconds after the page has loaded. Links in the sidebar do not get updated until at least 2-4 seconds after the page has loaded. Same applies if you refresh the page or navigate to the /home or /index routes manually (via the URL bar at the top of the page). \$\endgroup\$ Commented Jan 14, 2020 at 8:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.