2

I'm trying to learn react and fairly new to the framework. I am trying to create a simple navbar component wih material-ui that is responsive (will show all links on medium devices and up, and open a side drawer on small devices). I have most of it setup to my liking, however, the issue I am currently having, is getting and setting the active link according to the page I am on.

It seems to works correctly on the medium devices and up, but when transitioning to a smaller device, the link is not updated correctly, as it will keep the active link from the medium screen set, while updating the side drawer active link.

Navbar.js

const Navbar = () => {
    const classes = useStyles();
    const pathname = window.location.pathname;
    const path = pathname === '' ? '' : pathname.substr(1);
    const [selectedItem, setSelectedItem] = useState(path);

    const handleItemClick = (event, selected) => {
        setSelectedItem(selected);
        console.log(selectedItem);
    };

    return (
        <>
            <HideNavOnScroll>
                <AppBar position="fixed">
                    <Toolbar component="nav" className={classes.navbar}>
                        <Container maxWidth="lg" className={classes.navbarDisplayFlex}>
                            <List>
                                <ListItem
                                    button
                                    component={RouterLink}
                                    to="/"
                                    selected={selectedItem === ''}
                                    onClick={event => handleItemClick(event, '')}
                                >
                                    <ListItemText className={classes.item} primary="Home" />
                                </ListItem>
                            </List>

                            <Hidden smDown>
                                <List
                                    component="nav"
                                    aria-labelledby="main navigation"
                                    className={classes.navListDisplayFlex}
                                >
                                    <ListItem
                                        button
                                        component={RouterLink}
                                        to="/account/login"
                                        selected={selectedItem === 'account/login'}
                                        onClick={event => handleItemClick(event, 'account/login')}
                                    >
                                        <ListItemText className={classes.item} primary="Login" />
                                    </ListItem>

                                    <ListItem
                                        button
                                        component={RouterLink}
                                        to="/account/register"
                                        selected={selectedItem === 'account/register'}
                                        onClick={event => handleItemClick(event, 'account/register')}
                                    >
                                        <ListItemText className={classes.item} primary="Register" />
                                    </ListItem>

                                </List>
                            </Hidden>

                            <Hidden mdUp>
                                <SideDrawer />
                            </Hidden>
                        </Container>
                    </Toolbar>
                </AppBar>
            </HideNavOnScroll>

            <Toolbar id="scroll-to-top-anchor" />

            <ScrollToTop>
                <Fab aria-label="Scroll back to top">
                    <NavigationIcon />
                </Fab>
            </ScrollToTop>
        </>
    )
}

SideDrawer.js

const SideDrawer = () => {
  const classes = useStyles();
  const [state, setState] = useState({ right: false });

  const pathname = window.location.pathname;
  const path = pathname === "" ? "" : pathname.substr(1);
  const [selectedItem, setSelectedItem] = useState(path);

  const handleItemClick = (event, selected) => {
    setSelectedItem(selected);
    console.log(selectedItem);
  };

  const toggleDrawer = (anchor, open) => (event) => {
    if (
      event &&
      event.type === "keydown" &&
      (event.key === "Tab" || event.key === "Shift")
    ) {
      return;
    }

    setState({ ...state, [anchor]: open });
  };

  const drawerList = (anchor) => (
    <div
      className={classes.list}
      role="presentation"
      onClick={toggleDrawer(anchor, false)}
      onKeyDown={toggleDrawer(anchor, false)}
    >
      <List component="nav">
        <ListItem
          button
          component={RouterLink}
          to="/account/login"
          selected={selectedItem === "account/login"}
          onClick={(event) => handleItemClick(event, "account/login")}
        >
          <ListItemText className={classes.item} primary="Login" />
        </ListItem>

        <ListItem
          button
          component={RouterLink}
          to="/account/login"
          selected={selectedItem === "account/register"}
          onClick={(event) => handleItemClick(event, "account/register")}
        >
          <ListItemText className={classes.item} primary="Register" />
        </ListItem>
      </List>
    </div>
  );

  return (
    <React.Fragment>
      <IconButton
        edge="start"
        aria-label="Menu"
        onClick={toggleDrawer("right", true)}
      >
        <Menu fontSize="large" style={{ color: "white" }} />
      </IconButton>

      <Drawer
        anchor="right"
        open={state.right}
        onClose={toggleDrawer("right", false)}
      >
        {drawerList("right")}
      </Drawer>
    </React.Fragment>
  );
};

Code Sandbox - https://codesandbox.io/s/async-water-yx90j

I came across this question on SO: Is it possible to share states between components using the useState() hook in React?, which suggests that I need to lift the state up to a common ancestor component, but I don't quite understand how to apply this in my situation.

Jonathan Hall
  • 75,165
  • 16
  • 143
  • 189
JLI_98
  • 345
  • 2
  • 10
  • If you deliberately don't want to put your state into a parent component, e.g. because it's already very large/cluttered, you can also try the useImperativeHandle hook in combination with useForwardRef. These allow you to write components that are atomic. I.e. you can expose functions to change the state without lifting the state from the component to the parent. – JaB Nov 11 '22 at 08:30

1 Answers1

3

I would suggest to put aside for a moment your code and do a playground for this lifting state comprehension. Lifting state is the basic strategy to share state between unrelated components. Basically at some common ancestor is where the state and setState will live. there you can pass down as props to its children:

const Parent = () => {

  const [name, setName] = useState('joe')

  return (
    <>
      <h1>Parent Component</h1>
      <p>Child Name is {name}</p>
      <FirstChild name={name} setName={setName} />
      <SecondChild name={name} setName={setName} />
    </>
  )
}

const FirstChild = ({name, setName}) => {
  return (
    <>
      <h2>First Child Component</h2>
      <p>Are you sure child is {name}?</p>
      <button onClick={() => setName('Mary')}>My Name is Mary</button>
    </>
  )
}

const SecondChild = ({name, setName}) => {
  return (
    <>
      <h2>Second Child Component</h2>
      <p>Are you sure child is {name}?</p>
      <button onClick={() => setName('Joe')}>My Name is Joe</button>
    </>
  )
}

As you can see, there is one state only, one source of truth. State is located at Parent and it passes down to its children. Now, sometimes it can be troublesome if you need your state to be located at some far GreatGrandParent. You would have to pass down each child until get there, which is annoying. if you found yourself in this situation you can use React Context API. And, for most complicated state management, there are solutions like redux.

buzatto
  • 9,704
  • 5
  • 24
  • 33