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().
Related
I'm trying to setup a multi-level browsing pattern with React Router 6.3.0 on React 18.2.0. The pattern I'm trying to get to allows for drilling down into locations with nested paths such as:
/explore
/explore/USA
/explore/USA/Florida
The same component is able to process any of the routes by taking everything beyond the base as additional parameters. The routes are setup like this:
<BrowserRouter>
<Routes>
<Route path='/explore'>
<Route index element={<ExplorePath />} />
<Route path=':p1' element={<ExplorePath />} />
<Route path=':p1/:p2' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4/:p5' element={<ExplorePath />} />
<Route path=':p1/:p2/:p3/:p4/:p5/:p6' element={<ExplorePath />} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</BrowserRouter>
Sounds simple. Drilling down works fine. As I go to each deeper level the content renders as expected. The problem starts when trying to move up the train. I have breadcrumbs setup with the <Link> compontent. If I click on one for a higher level (such as "/explore/USA" when I'm looking at "/explore/USA/Florida") the URL updates as expected but the content does not update at all.
The only workaround I've found so far is to add the reloadDocument to my <Link> tags. This works, but it forces a page reload every time I navigate.
Is there a way to configure the routes to make the component re-render without forcing a full page reload?
Update 21-Jul-2022:
It isn't a fix, but I did find a workaround. Digging deeper, this seems to be an issue with how a React function and a React component work. First off, extracting route parameters in a component appears to be very messy (if possible at all). To get around it I created the following React function:
export function ExplorePath() {
const { p1, p2, p3, p4, p5, p6 } = useParams();
const parts = [p1, p2, p3, p4, p5, p6];
const trimmed = parts.filter(o => o !== undefined);
const path = trimmed.join('/');
return (
<Explore path={path} />
);
}
The function is being called any time I click a link that calls one of the defined routes. However, it appears that when the function calls the component it is reusing the component when navigating to a shorter path instead of creating a new one. As I drill down the tree I always end up with a new component. The workaround I ended up with is to check for a change in the props with componentDidUpdate like this:
constructor(props) {
super(props);
this.state = { isLoading: true, model: null };
}
componentDidMount() {
this.populateModel();
}
componentDidUpdate(prevProps: PageProps) {
if (this.props.path !== prevProps.path) {
this.setState({ isLoading: true, model: null });
this.populateModel();
}
}
So, did I find a bug in how React functions and components interact or is this just a quirk that I'm not understanding?
I am developing a React js blog-like web site and I am finding some trouble managing the routes. My pages with articles' URLs are like this: website/pages/3
I would like to redirect to homepage when the page index is 1, since the homepage is the first page with articles by default.
My Router looks like this:
<Router>
<Switch>
<Route exact path="/" render={() => <Page postsPerPage={3}/>} />
<Route exact path="/Page/:activePage" render={() => <Page postsPerPage={3}/>} />
<Route path="/Login" component={Login} />
<PrivateRoute path="/AddPost" component={AddPost} />
<Route path="/:postLocation" component={Post} />
</Switch>
</Router>
I would like to route "/Page/:activePage" to the component the route "/" renders if the activePage is 1. So the component would be the same (Page), but the path would be different.
Could conditional rendering in the Router do the trick, and if so, how? I was thinking about something along these lines:
<Route exact path="/Page/:activePage" render={() =>
{
let {activePage} = useParams()
if (activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}/>
However it seems React is not happy about me using useParams there (there's a compilation error: React Hook "useParams" cannot be called inside a callback. React Hooks must be called in a React function component or a custom React Hook function react-hooks/rules-of-hooks)
I tested that snippet with a constant value of 1 instead of the activePage parameter and it does redirect so that basically leaves me with the question of how do I retrieve the parameter from the path?
Render function in this case The render prop function has access to all the same route props (match, location and history) as the component render prop.
so you can basically do something like.
<Route exact path="/Page/:activePage" render={(props) =>
{
if (props.match.params.activePage == 1) return (<Redirect to="/"/>)
else return(<Page postsPerPage={3}/>)
}
}
/>
Looking at your example case above, I will rather not redirect anything but carry the logic into the Page component. inside the componentDidMount or useEffect function that extracts the activePage param. I will check if it's 1(or whatever sentinal value you choose to use) Then I perform the logic that will have been performed by the home component else I proceed normally with the route. eg If you extracted it and do a fetch to the backend for the case where its, not 1, then when it's 1 you could just return from the function and it will work as if it were on the home page. alternatively, after the check, if it's 1 you could then redirect back to '/'.
You should probably handle the routing within your Pages component or if you prefer, create a separate component to handle the conditional routing.
for example:
function Pages (props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : (
<div>
Your Pages component content here
</div>
)
}
export default Pages;
or
function ConditionalRoutingComponent(props) {
const {activePage} = useParams()
return activePage === 1 ? <Redirect to='/' /> : <Page postsPerPage={3}/>
}
export default ConditionalRoutingComponent;
Hope this helps
The render function of Component the is called with three parameters namely match, history and location. You can use them to perform the action you are trying to do with hooks.
<Route ... render={({match}) => {
if (match.params.activePage == 1) {
doYourStuff()
}
}}
I'm trying to implement React Router with query params like so http://localhost:3000/login?Id=1, I was able to achieve it only for my login route that too if I put path as http://localhost:3000/ which then redirects , however, I want to implement across the application. It matches nomatch route if I implement on other routes. This is how my index.js looks like, Can someone guide me how can i go about implementing all routes path including query params ?.
ReactDOM.render(
<BrowserRouter>
<Switch>
<Route
exact
path={`/`}
render={() => {
if (!store.getState().login.isAvailable) {
return <Redirect to={`/login?Id=${Id}`} />
} else {
return <Dashboard />
}
}}
/>
<Route exact path={`/login`} component={Login} />
<Route exact path={`/signup`} component={SignUp} />
{Routes.map((prop, key) => (
<Route path={prop.path} key={key} component={prop.component} />
))}
<Route component={NoMatch} />
</Switch>
</BrowserRouter>,
document.getElementById('root')
)
There are two ways about to accomplish what you want.
The most basic way would be on each "page" or root component of each route, handle the parsing of query params.
Any component that is the component of a Route component, will have the prop location passed to it. The query params are located in location.search and that will need to be parsed. If you are only worried about modern browsers, you can use URLSearchParams, or you can use a library like query-string, or of course, you can parse them yourself.
You can read more in the react-router docs.
The second way of doing this, really isn't that different, but you can have a HOC that wraps around each of your "pages" that handles the parsing of the query params, and passes them as a list or something to the "page" component in question.
Here's an example of the basic way using URLSearchParams:
import React from "react";
import { BrowserRouter as Router, Route, Link } from "react-router-dom";
// this is your "page" component, we are using the location prop
function ParamsPage({ location }) {
// you can use whatever you want to parse the params
let params = new URLSearchParams(location.search);
return (
<div>
<div>{params.get("name")}</div>
// this link goes to this same page, but passes a query param
Link that has params
</div>
);
}
// this would be equivalent to your index.js page
function ParamsExample() {
return (
<Router>
<Route component={ParamsPage} />
</Router>
);
}
export default ParamsExample;
EDIT: and to clarify, you don't need to do anything on your index.js page to make this work, the simple Routes you have should work fine.
I am having a react-redux app and react-router v4 inside of app
Is there a way to catch all invalid URLs that were entered and save them to an array, like so ['https://mysite.co/progects', 'https://mysite.co/sometypo', 'https://mysite.co/something']?
And then I want to send that data to server for building some redirects and some sitemap
Currently I have this:
<Switch>
{/* <Route path='/blog' exact component={Blog} /> */}
<Route path='/projects/:id' component={ProjectDetails} />
<Route path='/career/:id' component={CareerDetails} />
<Route path='/apply-for-job' render={(props) => (
<ModalWindow
{...props}
modalHeader='Apply form'>
<ApplyForm history={props.history} />
</ModalWindow>
)} />
<Route exact path='/' component={withScrollPreservation(LandingPage)} />
<Route component={NoMatch} />
{/* <Route component={withScrollPreservation(LandingPage)} /> */}
</Switch>
In your NoMatch component, you can have the logic to update unmatched/incorrect urls
class NoMatch extends React.Component {
componentDidMount() {
const { addNoMatchUrl } = this.props;
// you might want to handle the duplicate url logic here too in addNoMatchUrl method
addNoMatchUrl(this.props.location.pathname);
}
componentDidUpdate(prevProps) {
const { location, addNoMatchUrl } = this.props;
if (location.pathname !== prevProps.location.pathname) {
addNoMatchUrl(location.pathname);
}
}
render() {
// render logic
}
}
export default connect(null, {addNoMatchUrl});
If you want, say, to redirect someone, who typed '/progects' to '/projects' - well, that's nice UX, but your Switch block will be cluttered with tens of possible invalid urls.
As I see it, maybe you should add <Redirect to='/main' /> at the bottom of your Switch so any invalid url gets redirected to Main component (or whichever you have) or to 404-Component.
If you still want to gather them, then instead of redirecting to Main or 404 Component, send them to specific Error component, where you can get the link via this.props.history.location and handle that link further in the component: send to server, set that url in local/session storage, etc.
Note that you'll need a way to store that data someplace which won't get cleared on unmounting.
In order to send users to that route you'll need to place that at the bottom of your Switch.
<Switch>
...{your routes}
<Route component={Error} />
</Switch>
So actually all you need to do is handle those urls in your NoMatch component, like so
const path = this.props.history.location;
axios.post('your api', path);
When a url param change, I need to update two components, but one of them is outside the route with the param. The routes in App.js are like this:
<BrowserRouter>
<div>
<Route exact path="/" render={ (props) =>
<Home products={this.state.products} }
/>
<Route path="/products/:product" render={ (props) =>
<Product {...props} /> }
/>
<Route path="/" render={ props =>
<ProductHistory {...props}/> }
/>
</div>
</BrowserRouter>
The ProductHistory which is always visible has links pointing to products, like:
<Link to={`/products/${product.product_id}`}> {product.name}</Link>
When following such a link, the Product component is updated using ComponentWillReceiveProps method:
componentWillReceiveProps(nextProps) {
if(nextProps.match.params.product !== this.props.match.params.product){
But how do I update the ProductHistory component at the same time when the product param change? Since it isn't within the /products/:product route, checking this.props.match.params.product in ProductHistory's componentWillReceiveProps results in undefined.
(edit - and withRouter doesn't help, since it already is within a route, but a different one: "/")
In componentWillReceiveProps I could use location.pathname to check that the path begins with "/product", and I could find the param by substr(path.lastIndexOf('/') + 1.
Edit: But I also have to compare the current id param with the next product id param to avoid unnecessary updates. But when clicking the link, the url have already changed when componentWillReceiveProps fires so location.pathname and nextProps.location.pathname always match, so it updates unnecessarily (repeated api calls).
So I would have to find a different solution - rearrange the routing in some way? The idea is that ProductHistory should always be visible though.
You can render the Route simply like this:
<BrowserRouter>
<div>
<Switch>
<Route exact path="/" render={ (props) =>
<Home products={this.state.products} }
/>
<Route path="/products/:product" render={ (props) =>
<Product {...props} /> }
/>
</Switch>
<ProductHistory />
</div>
</BrowserRouter>
And then in the ProductHistory class you use the withRouter HOC
You can get access to the history object's properties and the closest
Route's match via the withRouter higher-order component. withRouter
will pass updated match, location, and history props to the wrapped
component whenever it renders.
example:
class ProductHistory extends Component { ... }
export default withRouter(ProductHistory);
or using decorators
#withRouter
export default class ProductHistory extends Component { ... }
With this you will be able to access match, location and history through props like this:
this.props.match
this.props.location
this.props.history
For anyone stumbling across this, there is a new solution afforded by hooks, in the form of useRouteMatch in react-router-dom.
You can lay your code out like João Cunha's example, where ProductHistory is not wrapped within a Route. If the ProductHistory is anywhere else but inside the Route for products, all the normal routing information will seemingly give the wrong answer (I think this might have been where the problems with withRouter arose in the replies to João's solution), but that's because it's not aware of the product route path spec. A little differently from most MVC routers, React-router-dom won't be calculating the route that matched up front, it will test the path you're on with every Route path spec and generate the specific route matching info for components under that Route.
So, think of it in this way: within the ProductHistory component, you use useRouteMatch to test whether the route matches a path spec from which you can extract the params you require. E.g.
import { useRouteMatch } from 'react-router-dom';
const ProductHistory = () => {
const { params: { product } } = useRouteMatch("/products/:product");
return <ProductList currentProduct={product || null} />;
};
This would allow you to test and match against multiple URLs that might apply to products, which makes for a very flexible solution!