React router: How to update component outside route on param change? - reactjs

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!

Related

Creating wrapper component for react-router-dom v6 Route that logs route changes

We are looking to add custom logging to our react application, and would like to log each time a user changes routes. To handle this, we are creating a wrapper component , and this is what we currently have:
import React, { useEffect } from 'react';
import { Route } from 'react-router-dom';
function LoggerRoute(isExact, path, element) {
useEffect(() => {
// send route-change log event to our mongodb collection
}, []);
// And return Route
return (
isExact
? <Route exact path={path} element={element} />
: <Route exact path={path} element={element} />
);
}
export default LoggerRoute;
...and in our App.js file, we have changed the routes as such:
// remove this // <Route exact path='/tools/team-scatter' element={<TeamScatterApp />} />
<LoggerRoute isExact={true} path='/tools/team-scatter' element={<TeamScatterApp />} />
However, this throws the error Uncaught Error: [LoggerRoute] is not a <Route> component. All component children of <Routes> must be a <Route> or <React.Fragment>.
Additionally, it feels off passing props to a route, if possible we would prefer
<LoggerRoute exact path='/tools/team-scatter' element={<TeamScatterApp />} />
as the ideal way to call our LoggerRoute. We like the idea of a wrapper component, this way we don’t have to add logging into every component that our app routes to. However, I’m not sure if this wrapper component approach is possible if can only accept a component. How can we modify our LoggerRoute component to work / be better?
In react-router-dom#6 only Route and React.Fragment are valid children of the Routes component. Create either a wrapper component or a layout route component to handle listening for route path changes. Since you presumably want to do this for more than one route at-a-time I suggest the layout route method, but you can create a component that handles either.
Example:
import { Outlet, useLocation } from 'react-router-dom';
const RouteListenerLayout = ({ children }) => {
const { pathname } = useLocation();
useEffect(() => {
// send route-change log event to our mongodb collection
}, [pathname]);
return children ?? <Outlet />;
};
The children prop is used in the cases where you want to wrap individual routed components (i.e. the element prop) or an entire component, (*i.e. App) that is rendering a set of Routes. The Outlet component is used in the case where you want to conditionally include a subset of routes within a Routes component (i.e. some nested Route components).
Wrap the routes you want to listen to route changes for.
Examples:
<Routes>
<Route element={<RouteListenerLayout />}>
<Route path="path1" element={<SomeComponent />} />
<Route path="someOtherPath" element={<SomeOtherComponent />} />
... wrapped routes components with listener
</Route>
... routes w/o listener
</Routes>
or
<Routes>
<Route
path="/"
element={(
<RouteListenerLayout>
<SomeComponent />
</RouteListenerLayout>
)}
/>
</Routes>
or
<RouteListenerLayout>
<App />
</RouteListenerLayout>
These all assume the router is rendered higher in the ReactTree than RouteListenerLayout so the useLocation hook works as expected.
In v6, <Route> is a lot more strict than it was in v5. Instead of building wrappers for , it may be used only inside other <Routes> or <Route> elements. If you try to wrap a <Route> in another component it will never render.
What you should be doing instead is adding a wrapper component and leveraging it in the element prop on the route.
function App() {
return (
<Routes>
<Route path="/public" element={<PublicPage />} />
<Route
path="/route"
element={
<AddLogging>
<YourPage/>
</AddLogging>
}
/>
</Routes>
);
}
Edit: here is an example wrapper component based on your needs:
function AddLogging({children}) {
useEffect(() => {
// send route-change log event to our mongodb collection
// can use useLocation hook to get route to log
}, []);
return children;
}

Non-matching routes are rendered with React Router

I'm using React Router with TypeScript. Here's two routes, namely / and /pages/id.
class MyComponent extends React.Component
{
render()
{
return <BrowserRouter>
<div>
<Route exact path='/' children={
() =>
<TopPage />} />
<Route exact path='/pages/:id' children={
(props: RouteComponentProps<{id: string}>) =>{
console.log(props)
return <IndividualPage match={props.match} />}} />
</div>
</BrowserRouter>
}
}
I expect, since my Routes are exact:
when I access /, only TopPage component is rendered
when I access /pages/1, only IndividualPage component is rendered
when I access /no-pages, nothing is rendered
However, I observed:
when I access /, or /no-pages, IndividualPage component is rendered with match=null (causing Cannot read property 'params' of null error)
when I access /pages/1, both TopPage and IndividualPage components are rendered
What am I doing wrong?
There are four different ways to set the render component for a Route, some of which behave slightly differently than others. Setting the children prop to a render function is one of those ways, but it's not the one that you want based on the behavior you expect. Unlike the other methods, the contents of a children function are always rendered even if the current page is not a match. So you will have both components rendering on every page. Per the docs:
Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.
Instead you can use the render prop or the component prop. These will only render if the route is a match.
The default behavior of the Router is to render all Routes which match, not just the first. In practice, this is a non-issue with your example because you have two exact routes so there is no possibility that both will match. But as a habit I would recommend putting your Route components inside of a Switch component which will only render the first matching Route.
class MyComponent extends React.Component {
render() {
return (
<BrowserRouter>
<Switch>
<Route exact path="/" component={TopPage} />
<Route exact path="/pages/:id" component={IndividualPage} />
</Switch>
</BrowserRouter>
);
}
}
No typescript annotations are needed here because the Route component knows that RouteComponentProps will be passed to the component.

How do I achieve conditional routing based on the parameter in the requested route?

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()
}
}}

Difference between passing component to Route as prop and wrapping component in render function

What is the difference between routing to a component like this:
<Route path="coolPath" component={MyComponent} />
or
<Route path="coolPath" render={props => <MyComponent {...props} customProp="s" } />
To this:
<Route path"=coolPath">
<MyComponent />
</Route>
or
<Route path"=coolPath">
<MyComponent cusomProps="cp"/>
</Route>
first you should read through this site:
https://reacttraining.com/react-router/web/api/Route
But to explain, there's three things going on here, the first two are examples of routing with previous version of react-router (before v5) and the third is react-router (v5 - current) recommended approach.
1. Route with component
<Route path="/coolPath" component={MyComponent} />
This type of route renders the single component passed to the prop. If an inline function is passed to the Route's component prop, it will unmount and remount the component on every render via the use of React.createElement. This can be inefficient, and passing custom props via this method is only possible via an inline function. React Router's authors recommend using the render prop as opposed to the component prop for handling inline functions, as shown below.
2. Route with render
<Route path="/coolPath" render={props => <MyComponent {...props} customProp="s" } />
Instead of having a new React element created for you using the component prop with an inline function, this route type passes in a function to be called when the location matches and does not unmount a component and remount a brand new one during rerender. It's also much easier to pass custom props via this method.
3. Route with children as components
<Route path="/coolPath">
<MyComponent customProp="s" />
</Route>
This is currently the recommended approach to routing, the child components will be rendered when the path is matched by the router. It's also very easy to pass custom props with this method.
Keep in mind there is a fourth type, which is:
4. Route with children as function
From reacttraining.com:
import React from "react";
import ReactDOM from "react-dom";
import {
BrowserRouter as Router,
Link,
Route
} from "react-router-dom";
function ListItemLink({ to, ...rest }) {
return (
<Route
path={to}
children={({ match }) => (
<li className={match ? "active" : ""}>
<Link to={to} {...rest} />
</li>
)}
/>
);
}
ReactDOM.render(
<Router>
<ul>
<ListItemLink to="/somewhere" />
<ListItemLink to="/somewhere-else" />
</ul>
</Router>,
node
);
Sometimes you need to render whether the path matches the location or not. In these cases, you can use the function children prop. It works exactly like render except that it gets called whether there is a match or not.

How to pass query params in every single route path in react?

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.

Resources