2

I have a list of pageNames and a list of pageUrls. I would like to create a router that generates a react component for each page in the list and uses a navbar with router links. I think the problem lies in the forEach function but I am not sure.

Can anyone tell what I am doing wrong?

Although I didn't find it helpful, here is a similar question.

import React, { useState } from "react";
import {
  BrowserRouter as Router,
  Switch,
  Route,
  Link
} from "react-router-dom";

function App() {

  const routes = [
    {
      name: 'home',
      path: '/'
    },
    {
      name: 'blog',
      path: '/blog'
    }
  ]

  // translate (map) your array of objects into jsx
  const routeComponents = routes.map(({name, path}) => ((
    <Route key={name} path={path}>
      <h1>{name}</h1>
    </Route>
  )))
  
  return (
    <div className="App" >
      <Router>
        <Navbar routes = {routes} />
        <Switch>
          {routeComponents}          
        </Switch>
      </Router>
    </div>
  );
}

export default App;

function Navbar(props){
  const links = props.routes.map(({name, path}) => (
    <Link key={name} to={path}>{name}</Link>
  ))
    return(
        <div style={{justifyContent: "space-around", margin:"auto", display:"flex",justifyContent:"space-around",backgroundColor:"red"}}>
            {links}
        </div>
    )
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181
Jadon Erwin
  • 551
  • 5
  • 25

2 Answers2

1

You're correct. forEach is populating your routes variable, which is an array. What you want is to build some JSX. This is traditionally done via map. You also do not need the useState calls, and you'll want to combine your page names and urls into a single object for easier management.

Here's some pseudocode that will put you on the path...

  const routes = [
    {
      name: 'hello',
      path: '/'
    },
    {
      name: 'there',
      path: '/there'
    }
  ]

  // translate (map) your array of objects into jsx
  const routeComponents = routes.map(({name, path}) => (
    <Route key={name} path={path}/>
  ))

  // routeComponents is JSX. You originally had an array of JSX
  <Switch>
    {routeComponents}          
  </Switch>

  // Same thing for your links
  const navbar = routes.map(({name, path}) => (
    <Link key={name} to={path}>{name}</Link>
  ))

  <div>
    {navbar}          
  </div>

update some tweaks in above example to clarify it.

Update #2 Here's full code (jammed into 1 file). Updated to react router v6. test it here

import "./styles.css";

import React, { useState } from "react";
import {
  BrowserRouter as Router,
  Routes,
  Route,
  Link,
  Outlet
} from "react-router-dom";

function Navbar({ routes }) {
  const links = routes.map(({ name, path }) => (
    <Link key={name} to={path}>
      {name}
    </Link>
  ));
  return (
    <div
      style={{
        justifyContent: "space-around",
        margin: "auto",
        display: "flex",
        justifyContent: "space-around",
        backgroundColor: "red"
      }}
    >
      {links}
    </div>
  );
}

export default function App() {
  const routes = [
    {
      name: "home",
      path: "/"
    },
    {
      name: "blog",
      path: "/blog"
    },
    {
      name: "about",
      path: "/about"
    }
  ];

  // translate (map) your array of objects into jsx
  const routeComponents = routes.map(({ name, path }) => (
    <Route key={name} path={path} element={<h1>{name}</h1>} />
  ));

  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <Router>
        <Routes>{routeComponents}</Routes>
        <Navbar routes={routes} />
      </Router>
      {Outlet}
    </div>
  );
}
Dave
  • 7,552
  • 4
  • 22
  • 26
  • This looks great Dave. I would really like to use "useState" because I plan on making the router dynamic so that the user can add/remove pages. Would the above answer work if the routes object were managed by "useState"? – Jadon Erwin Jan 03 '22 at 19:54
  • 1
    Yes, you could fetch your routes inside useEffect and save them into a routes state. Don't manage 2 states (names and urls) though. Each route is an object with name/url as keys. So [routes, setRoutes] = useState() and call setRoutes() to your routes in useEffect() when you know what they are. Something like that... – Dave Jan 03 '22 at 20:00
  • Dave, sorry to unaccepted your answer. Using your modifications still gives me the same problem. The header won't change off of "home". – Jadon Erwin Jan 03 '22 at 20:23
  • 1
    The code you wrote for 'links' is correct. Just do the same for your 'routes'. The issue is that the {routes} in your code is an array. If there are other issues, it's in the routes/links code. – Dave Jan 03 '22 at 20:27
  • I've updated my code to use the map function for the routes. It still has the same behavior. I think something else is the problem. – Jadon Erwin Jan 03 '22 at 20:30
  • Because you're using react-router v5 the "/" route is matching first. Just make that the last element in your array and you'll see things working. Here's the example: https://codesandbox.io/s/quirky-breeze-4dmd3?file=/src/App.js that said, then 'home' will appear last, which isn't what you want, so you need to add an "order" key to the routes objects and sort them ... OR use react-router 6. The link in my example uses the latest react router and works they way you'd like. – Dave Jan 03 '22 at 22:14
1

The issue you likely are seeing is due to the way the Switch component works. It renders the first matching Route or Redirect component. This means that within the Switch component path order and specificity matters! The home path "/" matches any path, so will always be matched and rendered.

You can avoid this by reordering the routes from more specific to less specific, or since you allude to wanting the routes to be dynamically generated from user interaction and stored in a state, specify the exact prop for all routes so all matching occurs exactly and the order and specificity is negated.

function App() {
  const routes = [
    {
      name: 'home',
      path: '/',
    },
    {
      name: 'blog',
      path: '/blog',
    },
  ];

  const routeComponents = routes.map(({name, path}) => (
    <Route key={name} exact path={path}> // <-- exact prop to exactly match paths
      <h1>{name}</h1>
    </Route>
  ));
  
  return (
    <div className="App" >
      <Router>
        <Navbar routes={routes} />
        <Switch>
          {routeComponents}          
        </Switch>
      </Router>
    </div>
  );
}
Drew Reese
  • 165,259
  • 14
  • 153
  • 181