Different routers, different components, same path - reactjs

I'm new to react. My app has array of restaurants with different categories. User should be able to go to page which loads all restaurants by categories or be able to go to restaurant's page.
Path /eat-out loads all available categories with a few suggestions for restaurants.
Category page works fine, but when I try to enter restaurant page it still loads category component instead of restaurant's.
I need to have a path eat-out/categories or eat-out/restaurant, where each would load a different component.
return (
<content className="content">
<Switch>
<Route exact path="/" render={() => <Dashboard />} />
<Route exact path="/reservations" render={() => <Reservations />} />
<Route exact path="/eat-out" render={() => <EatOut />} />
<Route
exact
path="/eat-out/:category"
render={() => <CategoryRestaurants />}
/>
<Route
exact
path="/eat-out/:restaurant"
render={() => <RestaurantPage />}
/>
</Switch>
</content>
);
};
Is it possible to do such routing?

This is possible if you know what the categories and/or restaurants are, but probably not what you really want:
import React from "react";
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";
import "./styles.css";
const CategoryRestaurants = ({ category }) => <div>Category: {category}</div>;
const RestaurantPage = ({ restaurant }) => <div>Restaurant: {restaurant}</div>;
const KNOWN_CATEGORIES = ["cat1", "cat2", "cat3"];
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Router>
<Switch>
<Route
exact
path="/eat-out/:category"
render={(props) => {
// Use the list of known categories to decide where to go
const param = props.match.params.category;
return KNOWN_CATEGORIES.includes(param) ? (
<CategoryRestaurants category={param} />
) : (
<RestaurantPage restaurant={param} />
);
}}
/>
</Switch>
</Router>
</div>
);
}
It would likely be better to have more defined URLs, such as /eat-out/category/:category and /eat-out/restaurant/:restaurant.

There are a few approaches here that might work for you.
Pattern Matching
If the values for category and restaurant have different patterns that you can check with regex, you can use pattern matching for your URL parameters, like so:
path="/eat-out/:category([A-Za-z]+)"
path="/eat-out/:restaurant([0-9]+)"
Static Value Matching
Or if you know all the values for category and restaurant ahead of time (i.e. the values are static and not pulled from the server/database), you can create a list like this and use regex:
const categories = ["Pizza", "Hamburgers", "Vegan"];
const categoryPattern = categories.join("|");
const restaurants = ["Tom's Sandwiches", "Super Restaurant Co."];
const restaurantPattern = restaurants.join("|");
path={`/eat-out/:category(${categoryPattern})`}
path={`/eat-out/:restaurant(${restaurantPattern})`}
Modify the Paths
If the values cannot be split by pattern and you don't know the values ahead of time and you don't mind changing the paths a bit, you can add a path before the URL parameter to tell the difference, like this:
path="/eat-out/categories/:category"
path="/eat-out/restaurants/:restaurant"
Internal Component Logic
Otherwise, you can create a single component that uses more complex logic to tell the difference between a category and a restaurant internally, then return either <CategoryRestaurants> or <RestaurantPage> depending on which is relevant.
For example you can try fetching category data based on the name/ID and if that fails, then fetch restaurant data instead. Alternatively, you can make a single route in your API that accepts a name/ID and returns the correct object, then just figure out what object type you received on the front-end and render accordingly.
So you would have something like this:
<Route
path="/eat-out/:categoryOrRestaurant"
render={() => <CategoryOrRestaurantPage/>}
/>

Related

React router does not reload component if same element is used for two paths

I have the following two paths set up
<Route path="bookings/:bookingnumber" element={<Bookings />} />
<Route path="bookings" element={<Bookings />} />
In Bookings component I have conditional code written to check if a parameter :bookingnumber is passed, and then it only shows results relevant to that booking number.
Otherwise, if no parameter is passed, it shows all the results for all the bookings.
I am using Outlet to display these components in the main area, and the two paths can be chosen from a sidebar.
Let's say I have 10 results if I go to /bookings, and 2 results if I go to /bookings/123.
However, if I go to /bookings/123 first and then to /bookings it keeps on showing only 2 results (the component does not reload, however I can see the URL changing in the browser)
Issue
<Route path="bookings" element={<Bookings />} />
<Route path="bookings/:bookingnumber" element={<Bookings />} />
With the same component rendered on more than 1 route, if you navigate from one route to another for the same routes component, the routed component remains mounted.
Example: If navigating from "/bookings" to "/bookings/123", or "/bookings/123" to "/bookings", or "/bookings/123" to "/bookings/456" the Bookings component remains mounted. If you've any logic that depends on the bookingnumber route path parameter then the Bookings component needs to handle this in a useEffect hook with a proper dependency on the bookingnumber param.
Solution
Use a useEffect hook to "listen" for changes to the bookingnumber param value.
Example:
const Bookings = () => {
const { bookingnumber } = useParams();
useEffect(() => {
// initial render or booking number value updated
// run logic depending on bookingnumber value to update "results"
...
}, [bookingnumber]);
...
};
I've been struggling too with this issue, the solution is to add the key prop, which is used by react to understand when 2 components are different (otherwise it assumes they're the same and they will not be re-rendered).
Hence the solution in your case may look like:
<Route key={'single'} path="bookings/:bookingnumber" element={<Bookings />} />
<Route key={'all'} path="bookings" element={<Bookings />} />
<Route path="bookings" element={<BookingList />}>
<Route path=":bookingnumber" element={<ViewBooking />} />
</Route>
make two pages, first one will show all list of bookings and second one will show ID data
Hello I believe you are using react-router-dom v6, in this case you can wrap the child routes inside your main path route. And then give them the index(true|false) attribute to make sure the router understand if it is the exact index page or should render a page based on params.
Note: I copied the example from above, and added some lines to fix it!
<Route path="bookings">
<Route index={false} path=":bookingnumber" element={<ViewBooking />} />
<Route index={true} element={<BookingList />} />
</Route>
UPDATE:
You can handle a single component route with useParams hook.
Here what you're routing file should look like:
<Route path="bookings">
<Route index={false} path=":bookingnumber" element={<ViewBooking />} />
<Route index={true} element={<BookingList />} />
</Route>
And this is what your component should look like:
const MyComponent = () => {
const { bookingnumber } = useParams()
// if the path doesn't include the booking number then we should just render a normal page
if( !bookingnumber ) {
return <h1>This is a simple page</h1>
}
// if the page path does include the booking number then we can fetch data and return another result
// example you can fetch data based on this number
const [username, setUsername] = useState("")
useEffect(() => {
const fetchData = async () => {
const req = await fetch(`https://629c770de9358232f75b55dc.mockapi.io/api/v1/users/${bookingnumber}`)
const data = await req.json()
setUsername(data.name)
}
fetchData()
}, [username])
return (
<h1>Welcome mr.{username}</h1>
)
}

Create routes from a set of values

I would like to set the routes for the languages my webpage accepts, something like
import Component from '../src/component/component';
<Switch>
<Route exact path="/es" component={(props) => <Component language="es" />}/>
<Route exact path="/en" component={ (props) => <Component language="en" />}/>
</Switch>
But the languages that are accepted depend on a configuration file, and more can be added in the future. So I would like to be able to add these routes depending on the values of this file.
The option I've seen is to add the routes like this:
}/>
But don't want to accept any possibility, just some the ones I need.
Is there a way to create routes to accept several possible routes but not any route?
thanks.
You can write a simple mapping for the accepted languages and then loop over it to render the Routes
const languages = ['es', 'en'];
import Component from '../src/component/component';
...
<Switch>
{languages.map(lang => {
return <Route
key={lang}
exact
path={`/${lang}`}
component={(props) => <Component {...props} language={lang} />}/>
})}
</Switch>
The Switch-component in react router will render the first component that does not return null, so we can hijack this behavior to accomplish what you need.
First, we define an object mapping languages to components:
const langs = {
en: ComponentEn,
es: ComponentEs,
};
Then we can define a language selector component:
const LanguageSelector=({match})=>{
const language = match.params.language
const Component = langs[language];
if(Component) return <Component/>
else return null;
}
Now we can use it in our routing
<Switch>
<Route path="/:language" component={LanguageSelector}/>
<DefaultComponent/>
</Switch>
Of course you can replace the DefaultComponent with whatever you want

React pass an object to the route

I have this set up of my app in the ´app.js´ file I have my routers defined like this inside the render method
<HashRouter>
<StatusToast />
<Switch>
<Route
exact={true}
path="/"
render={props => (
<ImageScene{...props} title="Images & Information Search" />
)}
/>
<Route path="/case/:id/images" component={DetailsScene} />
</Switch>
</HashRouter>
from ImageScene on a table row click, I call a method like this:
this.props.history.push(/case/${id}/images)
this trigger a route and load DetailsScene where I can get the passed in id like this this.props.match.params.id all works without any problem so far.
My question is how can I pass a more than a string (the id) can I pass somehow the whole object to the route?
I have tried to do something like this for 2nd route instead of:
<Route path="/case/:id/images" component={DetailsScene} />
to set up on the ImageScene a method which can expose the selceted object, for now lets just do a simple one like:
export function getSelectedRow() {
return {
title: 'test'
}
}
and than set up the route like:
const object = getSelectedRow();
<Route path="/case/:id/images"
render={props => (<DetailsScene{...props} title={object.title} />
)}
/>
but I cannot make it work... any help would be graet, I'm totally new to react and the whole router.
you could add state to history.push (https://reacttraining.com/react-router/web/api/history) which is available for component being rendered (DetailsScene). Remember to wrap DetailsScene withRouter(...) to have history.location.state available in its props.

React Router 4 Pass props to dynamic component

I have a list of doctors and I am trying to dynamically render a details page when selected. I see most people recommend to pass props through the Route component, something like this:
<Route path={`${match.url}/:name`}
component={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
Though I'm not clear on where I should be executing this. I tried it in DoctorList and DoctorItem but that didn't work. So I've set the Route in the App component, and I am able select a doctor, which then renders the DoctorView component and display the match.params prop just fine. But how do I get the selected doctor data to DoctorView? I'm probably making this harder than it should be. Here is my code:
App.jsx
const App = () => {
return (
<div>
<NavigationBar />
<FlashMessagesList />
<Switch>
<Route exact path="/" component={Greeting} />
<Route path="/signup" component={SignupPage} />
<Route path="/login" component={LoginPage} />
<Route path="/content" component={requireAuth(ShareContentPage)} />
<Route path="/doctors" component={requireAuth(Doctors)} />
<Route path="/doctor/:name" component={requireAuth(DoctorView)} />
</Switch>
</div>
);
}
DoctorList.jsx
class DoctorList extends React.Component {
render() {
const { doctors } = this.props;
const linkList = doctors.map((doctor, index) => {
return (
<DoctorItem doctor={doctor} key={index} />
);
});
return (
<div>
<h3>Doctor List</h3>
<ul>{linkList}</ul>
</div>
);
}
}
DoctorItem.jsx
const DoctorItem = ({ doctor, match }) => (
<div>
<Link
to={{ pathname:`/doctor/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
{doctor.profile.first_name} {doctor.profile.last_name}
</Link>
</div>
);
DoctorView.jsx
const DoctorItem = ({ doctor, match }) => (
<div>
<Link
to={{ pathname:`/doctor/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
{doctor.profile.first_name} {doctor.profile.last_name}
</Link>
</div>
);
I have access to the list of doctors via Redux, I could connect the component, bring in the list and compare id’s but that feels like a lot of unnecessary steps.
But how do I get the selected doctor data to DoctorView?
Keep in mind that having paths like /items and /items/:id creates a scenario where you might be landing on the details page first.
Do you:
a) fetch all the items anyways because you might go back to the list page?
b) just fetch that the information for that one item?
Neither answer is "correct" but at the end of the day you have three possible pieces of information:
1) the item id
2) a single item
3) a list of items (which may or may not contain all of the information you need for the details page)
Wherever you want to display the full details of an item, it needs to have access to that item via props. Putting all of the item details in the url would be arduous, plus it would make it impossible to do situation A.
Since you're using redux, it makes perfect sense grab the details of the item from the identifier in the url
export default
connect((state, props) => ({
doctor: state.doctorList.find(doctor =>
doctor.id === props.match.params.id
)
}))(DoctorView)
Does ^ seems like too many extra steps?
While the answer above solves the issue perfectly, I just want to add that using an inline function with component is not advised by react-router
Instead of doing:
<Route path={`${match.url}/:name`}
component={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
You should instead use it like:
<Route path={`${match.url}/:name`}
render={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
This will prevent the same component from being created on every mount and instead use the same component and update the state accordingly.
Hope this will help someone

How to get params in parent route component

I'm building an app with React, React-Router (v5) and Redux and wonder how to access the params of the current URL in a parent route.
That's my entry, the router.js:
<Wrapper>
<Route exact path="/login" render={(props) => (
<LoginPage {...props} entryPath={this.entryPath} />
)} />
<Route exact path="/" component={UserIsAuthenticated(HomePage)} />
<Route exact path="/about" component={UserIsAuthenticated(AboutPage)} />
<Route path="/projects" component={UserIsAuthenticated(ProjectsPage)} />
</Wrapper>
And that's my ProjectsPage component:
class ProjectsPage extends PureComponent {
componentDidMount() {
this.props.fetchProjectsRequest()
}
render() {
console.log(this.props.active)
if (this.props.loading) {
return <Loading />
} else {
return (
<Wrapper>
<Sidebar>
<ProjectList projects={this.props.projects} />
</Sidebar>
<Content>
<Route exact path="/projects" component={ProjectsDashboard} />
<Switch>
<Route exact path="/projects/new" component={ProjectNew} />
<Route exact path="/projects/:id/edit" component={ProjectEdit} />
<Route exact path="/projects/:id" component={ProjectPage} />
</Switch>
</Content>
</Wrapper>
)
}
}
}
const enhance = connect(
(state, props) => ({
active: props.match,
loading: projectSelectors.loading(state),
projects: projectSelectors.projects(state)
}),
{
fetchProjectsRequest
}
)
export default withRouter(enhance(ProjectsPage))
The problem is, that the console.log output in my render method is {"path":"/projects","url":"/projects","isExact":false,"params":{}} although the URL is http://localhost:3000/projects/14.
I want to add an ID prop to my ProjectList to highlight the currently selected project.
I could save the ID of the project in a store inside my ProjectPage component, but I think this would be a bit confusing, especially because the URL has the information actually – so why should I write something in the store?
Another (bad?) approach would be to parse the location object the get the ID by myself, but I think there is a react-router/react-router-redux way to get the params at this point that I've overlooked.
#Kyle explained issue very well from technical perspective.
I will just focus on solution to that problem.
You can use matchPath to get id of selected project.
matchPath - https://reacttraining.com/react-router/web/api/matchPath
This lets you use the same matching code that uses except
outside of the normal render cycle, like gathering up data
dependencies before rendering on the server.
Usage in this case is very straight forward.
1 Use matchPath
// history is one of the props passed by react-router to component
// #link https://reacttraining.com/react-router/web/api/history
const match = matchPath(history.location.pathname, {
// You can share this string as a constant if you want
path: "/articles/:id"
});
let articleId;
// match can be null
if (match && match.params.id) {
articleId = match.params.id;
}
2 Use articleId in render
{articleId && (
<h1>You selected article with id: {articleId}</h1>
)}
I build a simple demo which you can use to implement the same functionality in your project.
Demo: https://codesandbox.io/s/pQo6YMZop
I think that this solution is quite elegant because we use official react-router API which is also used for path matching in router. We also don't use window.location here so testing / mocking will be easy if you export also raw component.
TLDR
React router match.params will be an empty object if your <Route /> path property doesn't include :params.
Solve this use case: You could use let id = window.location.pathname.split("/").pop(); in your parent route's component to get the id.
Detailed reason why not
If the path prop supplied to a <Route /> doesn't have any params, such as /:id, react router isn't going to doing the parsing for you. If you look in matchPath.js at line #56 you can start to see how the match prop is constructed.
return {
path: path, // the path pattern used to match
url: path === '/' && url === '' ? '/' : url, // the matched portion of the URL
isExact: isExact, // whether or not we matched exactly
params: keys.reduce(function (memo, key, index) {
memo[key.name] = values[index];
return memo;
}, {})
};
Then we can look at line #43 you can see that keys comes from _compilePath.keys. We can then look at the compilePath function and see that it uses pathToRegexp(), which will use stringToRegexp(), which will use tokensToRegExp() which will then mutate keys on line #355 with keys.push(token). With a <Route /> that has no params value in its path prop, the parse() function used in stringToRegexp() will not return any tokens and line #355 won't even be reached because the only tokens array will not contain any token objects.
So.... if your <Route /> doesn't have :params then the keys value will be an empty array and you will not have any params on match.
In conclusion, it looks like you're going to have to get the params yourself if your route isn't taking them into account. You could to that by using let id = window.location.pathname.split("/").pop().

Resources