javascriptreactjsreact-router-domnested-routesreact-routing

Nested routing in react


I'm trying to implement nested routing in my react app The routes in my root app.jsx file are defined like this.

    <>
      <Router>
        <DrawerContextProvider>
        <Appbar />
        <Switch>
          <Route exact path='/admin'>
            <Admin/>
          </Route>
          <Route path='/technician' component={Technician} />
          <Route path='/accountmanager' component={AccountManager} />
        </Switch>
        </DrawerContextProvider>
      </Router>
    </>

This is the code in my admin component

function Admin() {
    const classes = useStyles();
    const { open } = useContext(DrawerContext)
    const { url, path } = useRouteMatch();

    return (
        <>
            <SideMenu iconsandnames={iconsandnames} />
            <div>
                <main
                    className={clsx(classes.content, {
                        [classes.contentShift]: open,
                    })}
                >
                    <div className={classes.drawerHeader} />
                    <Switch>
                        <Route exact path={path} >
                            <Dashboard />
                        </Route>
                        <Route  path={`${path}/:id`} >
                            <Outlet />
                        </Route>
                    </Switch>
                </main>
            </div>
        </>
    )
}

function Outlet() {
    const { id } = useParams();
    console.log(id);

    return (
        <div>
          <h3>{id}</h3>
        </div>
      );

}

This is the code in my Side menu component

export default function MiniDrawer({ iconsandnames }, props) {
    const classes = useStyles();
    const theme = useTheme();
    const { url, path } = useRouteMatch();
    const { open, handleDrawerClose } = useContext(DrawerContext)


    return (
        <div >
            <Drawer
                variant="permanent"
                className={clsx(classes.drawer, {
                    [classes.drawerOpen]: open,
                    [classes.drawerClose]: !open,
                })}
                classes={{
                    paper: clsx({
                        [classes.drawerOpen]: open,
                        [classes.drawerClose]: !open,
                    }),
                }}
            >
                <div className={classes.toolbar}>
                    <IconButton onClick={handleDrawerClose}>
                        {theme.direction === 'rtl' ? <ChevronRightIcon /> : <ChevronLeftIcon />}
                    </IconButton>
                </div>
                <Divider />
                <List>
                    {iconsandnames.map((data) => (
                        data.text == "Home" ? (
                            <Link to={url}>
                            <ListItem button key={data.text} >
                                <ListItemIcon><Icon>{data.iconname}</Icon></ListItemIcon>
                                <ListItemText primary={data.text} />
                            </ListItem>
                        </Link>):
(                        <Link to={`${url}/${data.route}`}>
                            <ListItem button key={data.text} >
                                <ListItemIcon><Icon>{data.iconname}</Icon></ListItemIcon>
                                <ListItemText primary={data.text} />
                            </ListItem>
                        </Link>)
                    ))}
                </List>
            </Drawer>
        </div>
    );
}

this is the iconsandnames array I'm passing into the sidemenu

const iconsandnames = [
    {
        iconname: 'home',
        text: 'Home',
        route: 'home'
    },
    {
        iconname: 'rounded_corner',
        text: 'Projects',
        route: 'projects'
    },
    {
        iconname: 'account_box',
        text: 'Account managers',
        route: 'accountmanagers'
    },
    {
        iconname: 'build',
        text: 'Contractors',
        route: 'contractors'
    },
    {
        iconname: 'perm_identity',
        text: 'Clients',
        route: 'clients'
    },
    {
        iconname: 'work_outline',
        text: 'Work Orders',
        route: 'workorders'
    },
    {
        iconname: 'preview',
        text: 'Additional',
        route: 'additional'
    },
]

http://localhost:3000/admin shows me enter image description here

http://localhost:3000/admin/projects shows me enter image description here

ideally the sidemenu should not disappear when I switch the route but instead it is disappearing over here. Any ideas on what might be the problem and how can I solve it?


Solution

  • Issue

    You are specifying the exact prop on the "/admin" path, so any nested routes can no longer be matched and rendered.

    <Route exact path='/admin'>
      <Admin/>
    </Route>
    

    Admin

    <Switch>
      <Route exact path={path} > // <-- can match
        <Dashboard />
      </Route>
      <Route  path={`${path}/:id`} > // <-- can't match
        <Outlet />
      </Route>
    </Switch>
    

    Solution

    When rendering Route components into a Switch, path order and specificity matter. You will want to order your paths from more specific to less specific, in order to give the more specific paths a chance to be matched and rendered first before less specific paths.

    Routes - remove the exact prop from the "/admin" route so any nested route can be matched.

    <>
      <Router>
        <DrawerContextProvider>
        <Appbar />
        <Switch>
          <Route path='/admin'>
            <Admin/>
          </Route>
          <Route path='/technician' component={Technician} />
          <Route path='/accountmanager' component={AccountManager} />
        </Switch>
        </DrawerContextProvider>
      </Router>
    </>
    

    Admin - reorder the routes by specificity.

    <Switch>
      <Route path={`${path}/:id`} >
        <Outlet />
      </Route>
      <Route path={path} >
        <Dashboard />
      </Route>
    </Switch>