Search code examples
reactjsreact-hooksreact-modal

React.js - understanding hooks and their rules


I'm new to learning react and I'm trying to understand hooks but keep coming unstuck with the Rules of Hooks.

I have an app that I'm building where it has a navigation bar down the left hand side with icons that open a modal when clicked on:

enter image description here

I'm now trying to set up the 'modal wrapper' to contain each of the modals using this codesandbox as an example (from this stack overflow answer), however I'm running into issues with the Rules of Hooks and coming unstuck.

I've been reading up about reach hooks in the documentation here and understand the theory of only calling hooks at the top level and in functions but as there are quite a few hooks in my code, I'm getting confused between which hooks should be called where and how to then link back to them.

I'm also a bit confused by the uses of classes vs. functions as the example I'm trying to follow makes us of a class and I'm used to coding using functions.

My current code is below:

import React, { Component } from 'react'
import { Router, Link } from "react-router-dom";
import Modal from "react-modal";
import { createBrowserHistory } from 'history'
import { useAuth0 } from "@auth0/auth0-react";
import LogoutButton from "../Logout-Button";
import LoginButton from "../Login-Button";
import { Nav } from "react-bootstrap";

import Auth0ProviderWithHistory from '../../auth0-provider-with-history'

import UserSettings from "./UserSettings";
import Dashboard from "./Dashboard/Dashboard";
import MEMsLine from "./MemsLine";
import MEMsGrid from "./MemsGrid";
import Copyright from "../Copyright";

import clsx from 'clsx'
import { makeStyles } from '@material-ui/core/styles'
import {
    CssBaseline,
    Drawer,
    Box,
    AppBar,
    Toolbar,
    List,
    Divider,
    IconButton,
    Container,
    ListItem,
    ListItemText,
    ListItemIcon,
} from '@material-ui/core'
import {
    ArrowBackIos as ArrowBackIcon,
    Menu as MenuIcon,
    Dashboard as DashboardIcon,
    History as MEMsIcon,
    People as PeopleIcon,
    Place as PlaceIcon,
    Cake as EventIcon,
    LibraryMusic as MusicIcon,
    Tv as TVIcon,
    LocalMovies as MovieIcon,
    SportsEsports as GameIcon,
    Timeline as MEMslineIcon,
    Settings as SettingsIcon,
} from '@material-ui/icons'

const history = createBrowserHistory();

const drawerWidth = 200

const useStyles = makeStyles((theme) => ({
    root: {
        display: 'flex',
    },
    toolbar: {
        paddingRight: 24, // keep right padding when drawer closed
    },
    toolbarIcon: {
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'flex-end',
        padding: '0 8px',
        ...theme.mixins.toolbar,
    },
    appBar: {
        zIndex: theme.zIndex.drawer + 1,
        transition: theme.transitions.create(['width', 'margin'], {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
        }),
    },
    appBarShift: {
        marginLeft: drawerWidth,
        width: `calc(100% - ${drawerWidth}px)`,
        transition: theme.transitions.create(['width', 'margin'], {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.enteringScreen,
        }),
    },
    menuButton: {
        marginRight: 36,
    },
    menuButtonHidden: {
        display: 'none',
    },
    title: {
        flexGrow: 1,
    },
    drawerPaper: {
        position: 'relative',
        whiteSpace: 'nowrap',
        backgroundColor: 'black',
        color: 'white',
        width: drawerWidth,
        transition: theme.transitions.create('width', {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.enteringScreen,
        }),
    },
    drawerPaperClose: {
        overflowX: 'hidden',
        transition: theme.transitions.create('width', {
            easing: theme.transitions.easing.sharp,
            duration: theme.transitions.duration.leavingScreen,
        }),
        paddingLeft: '8px',
        width: theme.spacing(7),
        [theme.breakpoints.up('sm')]: {
            width: theme.spacing(9),
        },
    },
    appBarSpacer: theme.mixins.toolbar,
    content: {
        flexGrow: 1,
        height: '100vh',
        overflow: 'auto',
    },
    container: {
        paddingTop: theme.spacing(4),
        paddingBottom: theme.spacing(4),
    },
    paper: {
        padding: theme.spacing(2),
        display: 'flex',
        overflow: 'auto',
        flexDirection: 'column',
    },
    fixedHeight: {
        height: 240,
    },
    navLink: {
        textDecoration: 'none',
        color: 'inherit',
    },
    appBarImage: {
        maxHeight: '75px',
        marginLeft: '-40px',
        paddingRight: '20px',
    },
    personIcon: {
        background: '#000000',
        display: 'flex',
        flexDirection: 'row',
    },
}))

class UserAccount extends Component {

    state = {
        loginOpened: false,
        signupOpened: false
    };

    openModal = modalType => () => {
        if (modalType === "login") {
            this.setState({
                loginOpened: true,
                signupOpened: false
            });
        } else if (modalType === "signup") {
            this.setState({
                loginOpened: false,
                signupOpened: true
            });
        }
    };

    closeModal = modalType => () => {
        if (modalType === "login") {
            this.setState({
                loginOpened: false
            });
        } else if (modalType === "signup") {
            this.setState({
                signupOpened: false
            });
        }
    };

    render() {
        const { loginOpened, signupOpened } = this.state;

        const classes = useStyles()
        const [open, setOpen] = React.useState(false)

        const handleDrawerOpen = () => {
            setOpen(true)
        }

        const handleDrawerClose = () => {
            setOpen(false)
        }

        const AuthNav = () => {
            const { isAuthenticated } = useAuth0();

            return (
                <Nav className="justify-content-end">
                    {isAuthenticated ? <LogoutButton /> : <LoginButton />}
                </Nav>
            );
        };

        return (
            // <AuthConsumer>
            //   {({ user }) => (
            //     <Can
            //       role={user.role}
            //       perform="useraccount:visit"
            //       yes={() => (
            <Router history={history}>
                <Auth0ProviderWithHistory>
                    <div className={classes.root}>
                        <CssBaseline />
                        <AppBar style={{ background: '#000000' }}
                            position="absolute"
                            className={clsx(classes.appBar, open && classes.appBarShift)}
                        >
                            <Toolbar className={classes.toolbar}>
                                <IconButton
                                    edge="start"
                                    color="inherit"
                                    aria-label="open drawer"
                                    onClick={handleDrawerOpen}
                                    className={clsx(
                                        classes.menuButton,
                                        open && classes.menuButtonHidden
                                    )}
                                >
                                    <MenuIcon />
                                </IconButton>
                                <Link to="/" className={classes.navLink}>
                                    <img
                                        className={classes.appBarImage}
                                        src='https://storage.googleapis.com/mems-images/mems-logo-small-rounded.png'
                                        alt="mems logo"
                                    />
                                </Link>
                                <div className={classes.personIcon} style={{ width: '100%', justifyContent: 'flex-end' }}>
                                    <AuthNav />
                                </div>
                            </Toolbar>
                        </AppBar>
                        <Drawer
                            variant="permanent"
                            classes={{
                                paper: clsx(classes.drawerPaper, !open && classes.drawerPaperClose),
                            }}
                            open={open}
                        >
                            <div className={classes.toolbarIcon}>
                                <IconButton onClick={handleDrawerClose}>
                                    <ArrowBackIcon style={{ color: 'white' }} />
                                </IconButton>
                            </div>

                            <Divider />
                            <List>
                                <ListItem button>
                                    <ListItemIcon>
                                        <SettingsIcon style={{ color: 'white' }} />
                                        <UserSettings />
                                    </ListItemIcon>
                                    <ListItemText primary="Settings" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <DashboardIcon style={{ color: 'white' }} />
                                        <Dashboard />
                                    </ListItemIcon>
                                    <ListItemText primary="Dashboard" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <MEMslineIcon style={{ color: 'white' }} />
                                        <MEMsLine />
                                    </ListItemIcon>
                                    <ListItemText primary="MEMsLine" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <MEMsIcon style={{ color: 'white' }} />
                                        <MEMsGrid />
                                    </ListItemIcon>
                                    <ListItemText primary="All MEMs" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <EventIcon style={{ color: 'white' }} />
                                        <p>Events</p>
                                    </ListItemIcon>
                                    <ListItemText primary="Events" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <PeopleIcon style={{ color: 'white' }} />
                                        <p>People</p>
                                    </ListItemIcon>
                                    <ListItemText primary="People" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <PlaceIcon style={{ color: 'white' }} />
                                        <p>Places</p>
                                    </ListItemIcon>
                                    <ListItemText primary="Places" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <MusicIcon style={{ color: 'white' }} />
                                        <p>Music</p>
                                    </ListItemIcon>
                                    <ListItemText primary="Music" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <MovieIcon style={{ color: 'white' }} />
                                        <p>Movies</p>
                                    </ListItemIcon>
                                    <ListItemText primary="Movies" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <TVIcon style={{ color: 'white' }} />
                                        <p>TV Shows</p>
                                    </ListItemIcon>
                                    <ListItemText primary="TV Shows" />
                                </ListItem>

                                <ListItem button>
                                    <ListItemIcon>
                                        <GameIcon style={{ color: 'white' }} />
                                        <p>Games</p>
                                    </ListItemIcon>
                                    <ListItemText primary="Games" />
                                </ListItem>
                            </List>
                            <Divider />
                        </Drawer>
                        <main className={classes.content}>
                            <div className={classes.appBarSpacer} />
                            <Container maxWidth="lg" className={classes.container}>
                                <Modal isOpen={loginOpened} onRequestClose={this.closeModal("login")}>
                                    <h1>Login</h1>
                                    <button onClick={this.openModal("signup")}>Open Signup</button>
                                    <button onClick={this.closeModal("login")}>Close this modal</button>
                                </Modal>
                                <Modal isOpen={signupOpened} onRequestClose={this.closeModal("signup")}>
                                    <h1>Sign Up</h1>
                                    <button onClick={this.openModal("login")}>Open Login</button>
                                    <button onClick={this.closeModal("signup")}>Close this modal</button>
                                </Modal>
                                <button onClick={this.openModal("login")}>Open Login</button>
                                <button onClick={this.openModal("signup")}>Open Signup</button>
                                <Box pt={4}>
                                    <Copyright />
                                </Box>
                            </Container>
                        </main>
                    </div>
                </Auth0ProviderWithHistory>
            </Router >
            //       )}
            //       no={() => <Redirect to="/" />}
            //     />
            //   )}
            // </AuthConsumer>
        );
    }
}

export default UserAccount

Any pointers very welcome!


Solution

    1. I'd advise you to try learning more about the basics of React itself and/or javascript. So my suggestion would be to go a read more, start with simple stuff then move to complex ones.
    1. A class component should always have a render method and will hold the state as a property like the example below. Also, notice that the function toggle is written as toggle = () =>{} not toggle = () => () =>{} <- as this would mean you're returning a function instead of just opening the function body(learn more about arrow functions)
        class ClassComponent extends React.Component {
          state = {
            show: false
          };
        
          toggle = () => {
            const { show } = this.state;
            this.setState({ show: !show });
          };
        
          render() {
            const { show } = this.state;
            return (
              <div>
                <span>{show ? "Show Class" : "Hide Class"}</span>
                <button onClick={this.toggle}>Click here</button>
              </div>
            );
          }
        }
    
    1. A functional component, which in the past did not have the possibility to hold a state but now can through thanks to React hooks, as seen in the example below.
        function FunctionComponent(props) {
          const [show, setShow] = React.useState(false);
          const toggle = () => setShow(!show);
          return (
            <div>
              <span>{show ? "Show Function" : "Hide Function"}</span>
              <button onClick={toggle}>Click here</button>
            </div>
          );
        }
    

    Now after understanding this bit this is how you can fix your codebase:

    1. Your render method in a class component contains hooks, that is a nono as hooks can and may only be used with functional components
    2. Your class functions are returning functions instead of just letting the function to be called, so when you do this.closeModal("login") you have to call it again like this.closeModal("login")() then it will work.

    In summary, hooks can only be used in function components, they provide the same functionalities has class components have. You can set the state, listed to some lifecycle methods, and much more. As function components used to be used in the past just for rendering and probably do some simple logic now they can be used the same way a class component would.

    Link the code sandbox with a working example is here.