I have react router setup with route parameters :name
<Router>
<Switch>
<Route path="/" exact={true} component={Index} />
<Route path="/about/:name" component={About} />
<Route component={NotFound} />
</Switch>
</Router>
Using <Link to=, links from Index correctly route to eg /about/vinnie.
However, if the <Link to= component is on the About page, clicking the link merely updates the browser URL, but dies not re-render the correct page.
Any clues why this might be happening?
About.jsx
render () {
const id = this.props.match.params.name;
return (
<div className="Explore">
<div className="container">
<Api endpoint={[this._apiEndpoint(id, 'info')]} loadData={true}>
<CustomerInfo />
</Api>
...
Api.jsx
render () {
let showData = this.props.loadData ? null : <button onClick={() => this._loadButton()}>Show</button>;
return (
<span>
{this.props.children && React.cloneElement(this.props.children, {
apiData: this.state.rawData
})}
{showData}
</span>
);
};
CustomerInfo.jsx
class CustomerInfo extends React.Component {
constructor(props) {
super(props);
this.state = {
CustomerInfo: {}
}
}
componentWillReceiveProps(nextProps) {
if (this.props.apiData !== nextProps.apiData) {
this.setState({CustomerInfo: nextProps.apiData[0]});
}
}
render() {
...
I think you need to add the exact prop to your first <Route />. Otherwise your routes match Index and About and I think react-router intentionally renders both.
Related
In the following code, the url changes but the content doesn't rerender until manual refresh. What am I doing wrong here? I could use props.children or something but don't really want to. My understanding of is that it should render the content of the nested elements under .
const LandingPage = () => {
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet />
</div>
)
}
export default class Router extends Component<any> {
state = {
teams: [team1, team2, team3] as Team[]
}
public render() {
return (
<BrowserRouter>
<Routes>
<Route path='/' element={<LandingPage />} >
{
this.state.teams.map(team => {
const path = `/${team.name.toLowerCase()}`
return (
<Route path={path} element={
<BaseTeam
name={team.name}
TL={team.TL}
location={team.location}
members={team.members}
iconPath={team.iconPath}
/>
} />)
})
}
</Route>
</Routes>
</BrowserRouter>
)
}
}
Maybe signal to react library that a key has changed so that it needs to rerender the outlet
const LandingPage = () => {
const location = useLocation(); // react-router-dom
return (
<div>
<div>
buttons
<Button>
<Link to="/team1">team1</Link>
</Button>
<Button>
<Link to="/team2">team2</Link>
</Button>
<Button>
<Link to="/team3">team3</Link>
</Button>
</div>
<Outlet key={location.pathname}/>
</div>
)}
It seems the mapped routes are missing a React key. Add key={path} so each route is rendering a different instance of BaseTeam.
The main issue is that the BaseTeam component is the same "instance" for all the routes rendering it.
It should either also have a key prop specified so when the key changes BaseTeam is remounted and sets the name class property.
Example:
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path} // <-- add missing React key
path={path}
element={(
<BaseTeam
key={path} // <-- add key to trigger remounting
name={team.name}
/>
)}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
Or BaseTeam needs to be updated to react to the name prop updating. Use the componentDidUpdate lifecycle method to check the name prop against the current state, enqueue a state update is necessary.
Example:
class BaseTeam extends React.Component {
state = {
name: this.props.name
};
componentDidUpdate(prevProps) {
if (prevProps.name !== this.props.name) {
this.setState({ name: this.props.name });
}
}
render() {
return <div>{this.state.name}</div>;
}
}
...
<BrowserRouter>
<Routes>
<Route path="/" element={<LandingPage />}>
{this.state.teams.map((team) => {
const path = `/${team.name.toLowerCase()}`;
return (
<Route
key={path}
path={path}
element={<BaseTeam name={team.name} />}
/>
);
})}
</Route>
</Routes>
</BrowserRouter>
As you've found out in your code though, just rendering the props.name prop directly is actually the correct solution. It's a React anti-pattern to store passed props into local state. As you can see, it requires extra code to keep the props and state synchrononized.
In my ReactJS app, routes are configured in below way:
class App extends React.Component{
constructor(props){
super(props);
this.state={
isLoggedin: false
}
}
componentDidMount(){
if(localStorage.getItem('name'))
{
this.setState({
isLoggedin: true
})}
}
render(){
return(
<>
<Switch>
<Route exact path="/" render={()=><Login isLoggedin={this.state.isLoggedin} />} />
<Route exact path="/login" render={<Login isLoggedin={this.state.isLoggedin} />} />
<Route exact path="/home" render={()=><Home isLoggedin={this.state.isLoggedin} />} />
</Switch></>
);
}
}
In Login.js:
class Login extends React.Component{
render(){
if(this.props.isLoggedin) return <Redirect to="/home" />;
return(
<h1>Login here</h1>
);
}
}
In Home.js:
class Home extends React.Component{
render(){
if(!this.props.isLoggedin) return <Redirect to="/login" />;
return(
<h1>Home</h1>
);
}
}
So what this code will do is that when the user visits the /, it would first go to Login component and as soon as isLoggedin is set to true, it would redirect the user to Home.js. Same thing would happen if user is not loggedin and he tries to access /home, he would be redirected to /login. Since I am using local storage, all of this would happen in flash of eye. It is also working just fine.
But I doubt if it is the best method to achieve my goal. I want to know if there is any more advisable method to do this.
Thanks!!
A more advisable method would be to decouple the auth checks from the components and abstract this into custom route components.
PrivateRoute - if the user is authenticated then a regular Route is rendered and the props are passed through, otherwise redirect to the "/login" path for user to authenticate.
const PrivateRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Route {...props} /> : <Redirect to="/login" />;
};
AnonymousRoute - Basically the inverse of the private route. If the user is already authenticated then redirect them to the "/home" path, otherwise render a route and pass the route props through.
const AnonymousRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Redirect to="/home" /> : <Route {...props} />;
};
From here you render the Login and Home components into their respective custom route components.
<Switch>
<PrivateRoute
isLoggedIn={this.state.isLoggedIn} // *
path="/home"
component={Home}
/>
<AnonymousRoute
isLoggedIn={this.state.isLoggedIn} // *
path={["/login", "/"]}
component={Login}
/>
</Switch>
* NOTE: The isLoggedIn={this.state.isLoggedIn} prop is only required here since the isLoggedIn state resides in the App component. A typical React application would store the auth state in a React Context or in global state like Redux, and thus wouldn't need to be explicitly passed via props, it could be accessed from within the custom route component.
Full sandbox code:
const PrivateRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Route {...props} /> : <Redirect to="/login" />;
};
const AnonymousRoute = ({ isLoggedIn, ...props }) => {
return isLoggedIn ? <Redirect to="/home" /> : <Route {...props} />;
};
class Login extends Component {
render() {
return (
<>
<h1>Login here</h1>
<button type="button" onClick={this.props.login}>
Log in
</button>
</>
);
}
}
class Home extends Component {
render() {
return (
<>
<h1>Home</h1>
<button type="button" onClick={this.props.logout}>
Log out
</button>
</>
);
}
}
export default class App extends Component {
state = {
isLoggedIn: false
};
componentDidMount() {
if (localStorage.getItem("isLoggedIn")) {
this.setState({
isLoggedIn: true
});
}
}
componentDidUpdate(prevProps, prevState) {
if (prevState.isLoggedIn !== this.state.isLoggedIn) {
localStorage.setItem("isLoggedIn", JSON.stringify(this.state.isLoggedIn));
}
}
logInHandler = () => this.setState({ isLoggedIn: true });
logOutHandler = () => this.setState({ isLoggedIn: false });
render() {
return (
<div className="App">
<div>Authenticated: {this.state.isLoggedIn ? "Yes" : "No"}</div>
<ul>
<li>
<Link to="/">/</Link>
</li>
<li>
<Link to="/home">home</Link>
</li>
<li>
<Link to="/login">log in</Link>
</li>
</ul>
<Switch>
<PrivateRoute
isLoggedIn={this.state.isLoggedIn}
path="/home"
render={() => <Home logout={this.logOutHandler} />}
/>
<AnonymousRoute
isLoggedIn={this.state.isLoggedIn}
path={["/login", "/"]}
render={() => <Login login={this.logInHandler} />}
/>
</Switch>
</div>
);
}
}
In my project, I have to navigate from one component to other without changing the url. For this, I used MemoryRouter which worked as expected. But now the same idea should work for different routes i.e /one and /two. For example,
/one --> BrowserRouter (url change visible)
/
/first
/second
/third
/two --> BrowserRouter (url change visible)
/
/first
/second
/third
For the new visible routes, i.e /one and /two, the already established working Memoryroutes (i.e /, /first, /second, /third) should work properly with respective data as per provided in /one and /two.
I am struggling to include the MemoryRoutes inside the BrowserRoutes with the following structure code:
<BrowserRouter>
<Switch>
<Route path="/one" render={() => <MemoryComp configFor="Dave"></MemoryComp>}></Route>
<Route path="/two" render={() => <MemoryComp configFor="Mike"></MemoryComp>}></Route>
</Switch>
</BrowserRouter>
and then the MemoryComp holds:
<MemoryRouter history={customHistory}>
<Switch>
<Route path="/" render={(props) => <InitPage configFor={this.props.configFor} history={props.history}></InitPage>}></Route>
<Route path="/first" component={FirstPage}></Route>
<Route path="/second" component={SecondPage}></Route>
<Route path="/third" component={ThirdPage}></Route>
</Switch>
</MemoryRouter>
What I am trying to achieve:
To make the screens work with BrowserRouter --> MemoryRouter configuration.
To pass data from memory route to another memory route based on the main browser route. (Trying to use history to achieve the same)
Note:
This is more likely better to handle the browser routing stuff with server routing instead. Also, this seems can be achieved with any react-stepper plugin. But trying to understand what I am doing wrong here, just for learning purpose.
Here is the whole minimized code, available on Stackblitz (not working):
import React, { Component } from 'react';
import { render } from 'react-dom';
import './style.css';
import { BrowserRouter, MemoryRouter, Route, Switch } from 'react-router-dom';
import { createBrowserHistory } from "history";
const customHistory = createBrowserHistory();
class MemoryComp extends Component{
render(){
return(
<MemoryRouter history={customHistory}>
<Switch>
<Route path="/" render={(props) => <InitPage configFor={this.props.configFor} history={props.history}></InitPage>}></Route>
<Route path="/first" component={FirstPage}></Route>
<Route path="/second" component={SecondPage}></Route>
<Route path="/third" component={ThirdPage}></Route>
</Switch>
</MemoryRouter>
);
}
}
class InitPage extends Component{
render(){
return (
<>
<ul>
<li onClick={() => this.navigateTo("/")}>Init</li>
<li onClick={() => this.navigateTo("/first")}>First</li>
<li onClick={() => this.navigateTo("/second")}>Second</li>
<li onClick={() => this.navigateTo("/third")}>Third</li>
</ul>
<div>{this.props.configFor}</div>
</>
)
}
navigateTo(path){
this.props.history.push(path, {
data: {
configFor: this.props.configFor
}
})
}
}
class FirstPage extends Component{
constructor(props){
super(props);
this.data = this.props.history.location.state.data;
}
render(){
return (
<>
<ul>
<li onClick={() => this.navigateTo("/")}>Init</li>
<li onClick={() => this.navigateTo("/first")}>First</li>
<li onClick={() => this.navigateTo("/second")}>Second</li>
<li onClick={() => this.navigateTo("/third")}>Third</li>
</ul>
<div>first page</div>
</>
)
}
navigateTo(path){
this.props.history.push(path, {
data: {...this.data, ...{pageName: 'first'}}
})
}
}
class SecondPage extends Component{
constructor(props){
super(props);
this.data = this.props.history.location.state.data;
}
render(){
return (
<>
<ul>
<li onClick={() => this.navigateTo("/")}>Init</li>
<li onClick={() => this.navigateTo("/first")}>First</li>
<li onClick={() => this.navigateTo("/second")}>Second</li>
<li onClick={() => this.navigateTo("/third")}>Third</li>
</ul>
<div>second page</div>
</>
)
}
navigateTo(path){
this.props.history.push(path, {
data: {...this.data, ...{name: 'deducedValue'}}
})
}
}
class ThirdPage extends Component{
constructor(props){
super(props);
this.data = this.props.history.location.state.data;
}
render(){
return (
<>
<ul>
<li onClick={() => this.navigateTo("/")}>Init</li>
<li onClick={() => this.navigateTo("/first")}>First</li>
<li onClick={() => this.navigateTo("/second")}>Second</li>
<li onClick={() => this.navigateTo("/third")}>Third</li>
</ul>
<div>third page</div>
</>
)
}
navigateTo(path){
this.props.history.push(path, {
data: this.data
})
}
}
class App extends Component {
constructor() {
super();
this.state = {
name: 'React'
};
}
render() {
return (
<div>
<BrowserRouter>
<Switch>
<Route path="/one" render={() => <MemoryComp configFor="Dave"></MemoryComp>}></Route>
<Route path="/two" render={() => <MemoryComp configFor="Mike"></MemoryComp>}></Route>
</Switch>
</BrowserRouter>
</div>
);
}
}
render(<App />, document.getElementById('root'));
At first I thought there was something wrong with Switch in combination with MemoryRouter, but after some debugging I realized it's actually totally independent.
The problem you have is that your base memory route needs to be exact, otherwise all other routes will match that one and first ('/') will be returned. Just add exact to your base route.
<MemoryRouter>
<Switch>
<Route exact path="/" render={(props) => <InitPage configFor={this.props.configFor} history={props.history}></InitPage>}></Route>
<Route path="/first" component={FirstPage}></Route>
<Route path="/second" component={SecondPage}></Route>
<Route path="/third" component={ThirdPage}></Route>
</Switch>
</MemoryRouter>
Be careful if you have even more nested routes to always add exact to the root one e.g. /first needs to be exact in order for this to work correctly:
<Route exact path="/first" component={FirstPage}></Route>
<Route path="/first/nested-1" component={SecondPage}></Route>
<Route path="/first/nested-2" component={ThirdPage}></Route>
New to React:
I have a <Header /> Component that I want to hide only when the user visit a specific page.
The way I designed my app so far is that the <Header /> Component is not re-rendered when navigating, only the page content is, so it gives a really smooth experience.
I tried to re-render the header for every route, that would make it easy to hide, but I get that ugly re-rendering glitch each time I navigate.
So basically, is there a way to re-render a component only when going in and out of a specific route ?
If not, what would be the best practice to achieve this goal ?
App.js:
class App extends Component {
render() {
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
Main.js:
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route exact activeClassName="active" path="/art" component={Art} />
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);
I'm new to React too, but came across this problem. A react-router based alternative to the accepted answer would be to use withRouter, which wraps the component you want to hide and provides it with location prop (amongst others).
import { withRouter } from 'react-router-dom';
const ComponentToHide = (props) => {
const { location } = props;
if (location.pathname.match(/routeOnWhichToHideIt/)){
return null;
}
return (
<ComponentToHideContent/>
)
}
const ComponentThatHides = withRouter(ComponentToHide);
Note though this caveat from the docs:
withRouter does not subscribe to location changes like React Redux’s
connect does for state changes. Instead, re-renders after location
changes propagate out from the component. This means that
withRouter does not re-render on route transitions unless its parent
component re-renders.
This caveat not withstanding, this approach seems to work for me for a very similar use case to the OP's.
Since React Router 5.1 there is the hook useLocation, which lets you easily access the current location.
import { useLocation } from 'react-router-dom'
function HeaderView() {
let location = useLocation();
console.log(location.pathname);
return <span>Path : {location.pathname}</span>
}
You could add it to all routes (by declaring a non exact path) and hide it in your specific path:
<Route path='/' component={Header} /> // note, no exact={true}
then in Header render method:
render() {
const {match: {url}} = this.props;
if(url.startWith('/your-no-header-path') {
return null;
} else {
// your existing render login
}
}
You can rely on state to do the re-rendering.
If you navigate from route shouldHide then this.setState({ hide: true })
You can wrap your <Header> in the render with a conditional:
{
!this.state.hide &&
<Header>
}
Or you can use a function:
_header = () => {
const { hide } = this.state
if (hide) return null
return (
<Header />
)
}
And in the render method:
{this._header()}
I haven't tried react-router, but something like this might work:
class App extends Component {
constructor(props) {
super(props)
this.state = {
hide: false
}
}
toggleHeader = () => {
const { hide } = this.state
this.setState({ hide: !hide })
}
render() {
const Main = () => (
<Switch>
<Route exact activeClassName="active" path="/" component={Home} />
<Route
exact
activeClassName="active"
path="/art"
render={(props) => <Art toggleHeader={this.toggleHeader} />}
/>
<Route exact activeClassName="active" path="/about" component={About} />
<Route exact activeClassName="active" path="/contact" component={Contact} />
</Switch>
);
return (
<BrowserRouter>
<div className="App">
<Frame>
<Canvas />
<Header />
<Main />
<NavBar />
</Frame>
</div>
</BrowserRouter>
);
}
}
And you need to manually call the function inside Art:
this.props.hideHeader()
{location.pathname !== '/page-you-dont-want' && <YourComponent />}
This will check the path name if it is NOT page that you DO NOT want the component to appear, it will NOT display it, otherwise is WILL display it.
I have a router config like this:
<Router history={browserHistory}>
<Route path="/">
<IndexRoute component={HomePageContainers} />
<Route path="r/:subreddit_name" component={Subreddit} />
</Route>
</Router>
HomePageContainers renders like:
render() {
return (<div>
<HomePage reddits={this.state.reddits} key={this.state.isLoaded && this.state.reddits.data.after} isLoaded={this.state.isLoaded} isScrolling={this.state.isScrolling} scrollFunc={this.onScroll.bind(this)} />
</div>)
}
and HomePage render Reddits like:
<Col md={3} key={reddit.data.id}>
<Reddits key={reddit.data.id} reddit={reddit}/><br/>
</Col>
There is a Reddits class which looks like:
class Reddits extends React.Component {
constructor(props){
super(props);
}
render() {
let reddit = this.props.reddit;
return(
<div id={styles.box}>
<p>{reddit.data.title}</p>
<Link to={reddit.data.url}>
<p>{reddit.data.url}</p>
</Link>
</div>
)
}
}
whenever the path is r/:subreddit_name made, Subreddit component gets life and its working. Here I also get the :subreddit_name value as part of routeParams. But if you look at the Reddits class, the reddit.data has all the information for Subreddit.
I want to send the reddit.data as props from Route(r), is it possible?
You will want to have HomeComponenet render your Reddit component as children like so:
<Route path='/home' component="HomeComponent">
<Route path='/reddit' component="RedditComponent">
</Route>
To get reddit component to display you need to render children in your HomeComp:
render() {
{ this.props.children }
}
Now you can pass in props via cloning children like so:
HomeComp:
render() {
{this.props.children && React.cloneElement(this.props.children, {
someFunctionOrDataToPass: this.someFunctionOrDataToPass
})}
}
Here is another example