I have a App component with all the routes defined as below;
function App() {
//some logic for state including canShow which is a boolean and shows routes only if it is true
{canShow && (
<Route exact path="/Route1">
<Comp1 />
</Route>
<Route exact path="/Route2">
<Comp2 />
</Route>
)
}
Now say if user is currently on localhost/#/Route1 and manually enters URL i.e. say to localhost/#/Route2, the control seems to be jumping directly to Comp2
I also have props.history.listen() setup in a child component of App (outside all the Route definitions). So basically this listen is in a direct child component of App.
Is there any way by which on manually entering the URL, I can ensure that the control first always goes to App.js...So that I can update the logic for setting "canShow" and if canShow is false, I do not render any child component and also the control does not go Comp1 or Comp2
Also control to props.history.listen callback when navigating via links, but with direct URL entry, it does not seem to be going to props.history.listen first.
You can use the useHistory hook to let your App component update on history changes. This hook returns the history object with a location property.
In the App component, you can add an useEffect to determine your canShow state and update it when needed.
import { useHistory } from 'react-router';
import { useEffect, useState } from 'react';
function App() {
const [canShow, setCanShow] = useState(true);
const history = useHistory();
useEffect(() => {
if (history.location.pathname === '/Route1') {
setCanShow(true);
}
}, [history])
//some logic for state including canShow which is a boolean and shows routes only if it is true
return canShow && (
<Route exact path="/Route1">
<Comp1 />
</Route>
<Route exact path="/Route2">
<Comp2 />
</Route>
);
}
Related
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;
}
My App component definition looks as follows:
function App() {
return (
<Router>
<Navbar/>
<Switch>
<Route path="/howitworks">
<HowItWorks/>
</Route>
<Route path="/aboutus">
<AboutUs/>
</Route>
<Route path="/">
<Home/>
</Route>
</Switch>
<Footer/>
</Router>
)
}
I have a question regarding to route and re-render.
For example, when I route from / to /howitworks, then the component <HowItWorks/> is going to be rendered. Routing back to / from /howitworks, will <Home/> component be re-rendered?
The <Home/> component contains only text. It does not contain any logic.
Update
I have created an example on https://codesandbox.io/s/react-router-forked-2mp45.
When you consider the about component, how it is defined:
import React, { useState } from "react";
const About = () => {
const [state, _] = useState(2);
React.useEffect(
(_) => {
console.log("state changed");
},
[state]
);
return (
<div>
<h2>About</h2>
</div>
);
};
export default About;
and every time when /aboutus is clicked, it shows always the message:
state changed
that means for me, every time when the path changed, then re-render will always happen.
Am I right?
Yes, re-render happens on path change. Re-render essentially means painting the screen again.
If you think about component being mounting again the component unmounts too on path change.
Here is an example reflecting that https://codesandbox.io/s/react-router-forked-nlqzg?file=/components/About.js
Is there a way to replace everything from <Router history={hashHistory}> to </Router> with just a simple message such as <div>You don't have access to this section of the site.</div> based on certain conditions in MyApp component and re-render the DOM? See code below:
index.js:
import React from 'react';
import ReactDOM from 'react-dom';
import { Redirect, Router, Route, hashHistory } from 'react-router';
import MyApp from './ReactCode/MyApp';
import UsersComponent from './ReactCode/Users/UsersComponent';
import ProductsComponent from './ReactCode/Products/ProductsComponent';
ReactDOM.render((
<Router history={hashHistory}>
<Route path="/" component={MyApp}>
<Route path="/:link" component={UsersComponent} />
<Route path="/:link/products" component={ProductsComponent} />
<Redirect from="/:link/:product" to="/:link/products/?id=:product" />
</Route>
</Router>
), document.getElementById('root-container'));`
Yes, if you have a variable that represents that condition (let's call it condition), you can use it to put different components into a route.For example, you could use condition and a component Denied, which just renders <div>You don't have access to this section of the site.</div> to do something like this:
<Route path="/:link" component={condition ? UsersComponent : Denied} />
If you find yourself needing to do that a lot, you can even make a which includes that protection which you can use instead of Route:
const ProtectedRoute = ({ condition, path }) => <Route path={path} component={condition ? UsersComponent : Denied} />
That way, you can use ProtectedRoute instead of Route whenever you need to add that message.
EDIT
If you can only access your condition in componentDidMount, then you can set that condition in local state using setState:
// this should go somewhere in your component to define a default state
// here the default state is false, edit it to meet your needs
state = { condition: false };
componentDidMount() {
// however you're getting your condition goes here
// sets this.state.condition to whatever your condition is
this.setState({ condition });
}
You can then pass this.state.condition to ProtectedRoute in your render function:
<ProtectedRoute path="/:link" condition={this.state.condition} />
Note that this method will call render twice as per the React docs and can cause performance issues.
Problem: When I use history.push(), I can see that browser changes url, but it does not render my component listening on the path. It only renders if I refresh a page.
App.js file:
import React from "react";
import { BrowserRouter as Router, Route } from "react-router-dom";
import { Provider } from "react-redux";
import PropTypes from "prop-types";
//Components
import LoginForm from "../LoginForm/LoginForm";
import PrivateRoute from "../PrivateRoute/PrivateRoute";
import ServerList from "../ServerList/ServerList";
const App = ({ store }) => {
const isLoggedIn = localStorage.getItem("userToken");
return (
<Router>
<Provider store={store}>
<div className="App">
{isLoggedIn !== true && (
<Route exact path="/login" component={LoginForm} />
)}
<PrivateRoute
isLoggedIn={!!isLoggedIn}
path="/"
component={ServerList}
/>
</div>
</Provider>
</Router>
);
};
App.propTypes = {
store: PropTypes.object.isRequired
};
export default App;
Inside my LoginForm, I am making a request to an API, and after doing my procedures, I use .then() to redirect my user:
.then(() => {
props.history.push("/");
})
What happens: Browser changes url from /login to /, but component listening on / route is not rendered, unless I reload page.
Inside my / component, I use useEffect() hook to make another request to API, which fetches data and prints it inside return(). If I console.log inside useEffect() it happens twice, I assume initial one, and when I store data from an API inside component's state using useState() hook.
EDIT: adding PrivateRoute component as requested:
import React from "react";
import { Route, Redirect } from "react-router-dom";
const PrivateRoute = ({ component: Component, isLoggedIn, ...rest }) => {
return (
<Route
{...rest}
render={props =>
isLoggedIn === true ? (
<Component {...props} />
) : (
<Redirect to={{ pathname: "/login" }} />
)
}
/>
);
};
export default PrivateRoute;
What I tried already:
1) Wrapping my default export with withRouter():
export default withRouter(connect(mapStateToProps, mapDispatchToProps)(LoginForm));
2) Creating custom history and passing it as prop to Router.
react-router-dom version is ^5.0.1. react-router is the same, 5.0.1
You have at two mistakes in your code.
You are not using <switch> component to wrap routes. So all routes are processed at every render and all components from each <route> are rendered.
You are using local store to exchange information between components. But change in local store is invisible to react, so it does not fire component re-rendering. To correct this you should use local state in App component (by converting it to class or using hooks).
So corrected code will look like
const App = ({ store }) => {
const [userToken, setUserToken] = useState(localStorage.getItem("userToken")); // You can read user token from local store. So on after token is received, user is not asked for login
return (
<Router>
<Provider store={store}>
<div className="App">
<Switch>
{!!userToken !== true && (
<Route exact path="/login"
render={props => <LoginForm {...props} setUserToken={setUserToken} />}
/>
)}
<PrivateRoute
isLoggedIn={!!userToken}
path="/"
component={ServerList}
/>
</Switch>
</div>
</Provider>
</Router>
);
};
And LoginForm should use setUserToken to change user token in App component. It also may store user token in local store so on page refresh user is not asked for login, but stored token is used.
Also be sure not to put anything between <Switch> and </Switch> except <Route>. Otherwise routing will not work.
Here is working sample
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!