React Router: Route defined in child component not working - reactjs

I'm working on a React web application using React router.
The Route objects defined on the main wrapper component are working just fine, but if I try to define a Route on a child component, any link pointing to it won't be able to render the desired component.
Here is a code snippet trying to explain the situation:
class MainWrapper extends Component {
render() {
return (
<div className="App">
<Switch>
<Route exact path="/a" component= {A}/>
</Switch>
</div>
);
}
}
const A = () => {
return (
<div>
<Route exact path="/b" component={B}/>
<Link to="/b"/>
</div>
)
}
const B = () => {
return (<div>HELLO</div>)
}
In my application, the link pointing to "/b" is not rendering the B component, like the component prop weren't passed
Why this won't work?

You are specifying "exact path" in both Routes, so for rendering B your path should be exactly "/b", but when linking to "/b" component A will unmount because for rendering A you must be on exact path "/a". You should change your approach. One would be removing "exact" and including "/a" to your Link:
class MainWrapper extends Component {
render() {
return (
<div className="App">
<Switch>
<Route path="/a" component= {A}/>
</Switch>
</div>
);
}
}
const A = () => {
return (
<div>
<Route path="/b" component={B}/>
<Link to="/a/b"/>
</div>
)
}
const B = () => {
return (<div>HELLO</div>)
}

if B is a child of A, the url should be /a/b instead of /b, so you just need to update the A component with this code
const A = ({match}) => {
return (
<div>
<Route exact path={`${match.url}/b`} component={B}/>
<Link to=to={`${match.url}/b`}/>
</div>
)
};
See the documentation here

Do you have a Router somewhere? Also, you haven't closed your Link tag.

You need to wrap it in a Switch, and you should remove the exact prop from your /b route.
const A = ({match}) => {
return (
<div>
<Switch>
<Route path={`${match.url}/b`} component={B}/>
</Switch>
<Link to="a/b"/>
</div>
)
}

Related

<Outlet /> fails to rerender with react router v6

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.

React Link params of undefined

Params is undefined in ItemPage. I can't seem to see the issue here. Any takers?
Basically trying to create a dynamic path with Link
.
export default function App() {
return (
<Router>
<Switch>
<Route path="/" exact component={Home} />
<Route path="/catalog" component={Catalog} />
<Route path="/about" component={About} />
<Route path="/item/:nm" component={ItemPage}/>
<Route render={() => <h1>404: page not found</h1>} />
</Switch>
</Router>
)}
function Catalog() {
return (
<div className="Catalog">
{Routes.map((route, index) => {
return <p key={index}><Link to={`/item/${route.name}`}> {route.name} </Link></p>
})}
</div>
);
}
const ItemPage = ({match:{params:{nm}}}) => {
return (
<div>
<h1>Item {nm} Page</h1>
<ItemPage name={nm}/>
</div>
)
};
if you use react-router-dom package, then this method might help you then:-
ItemPage (use useParam hook):-
import { useParams } from 'react-router-dom'
const ItemPage = () => {
const { nm } = useParams()
return (
<div>
<h1>Item {nm} Page</h1>
<ItemPage name={nm}/>
</div>
)
};
I'm not sure if the way you have your props set up on ItemPage is exactly correct - in my experience using react-router I've always just used match as its own parameter without defining its properties like so:
const ItemPage = ({match}) => {
return (
<div>
<h1>Item {nm} Page</h1>
<ItemPage name={match.params.nm}/>
</div>
)
};
Another option could be to use the routecomponent props and do some destructuring to pull out match:
import { RouteComponentProps } from 'react-router';
const ItemPage : FC<RouteComponentProps> = (props) => {
const {match} = props
return (
<div>
<h1>Item {nm} Page</h1>
<ItemPage name={match.params.nm}/>
</div>
)
};
(see here: What TypeScript type should I use to reference the match object in my props?)
Basically the nested <ItemPage name={nm}/> was the issue. It should be replaced with a different component or HTML.
https://github.com/Tyler90901/webtech/pull/1/commits/16b337bc2e45ef0f06bc41f1fc060b87a8bc9d36

How to resolve undefined match props in React and React-router

When I click to the <Link to={"/weather/" + loc.id} onClick={props.findCurrentWeather}> in SearchBar component. I got the error :
Cannot read property 'params' of undefined
My App.js class component :
class App extends Component {
state = {
locations: undefined,
current: undefined,
...
}
findCurrentWeather = async () => {
let id = this.props.match.params.id; //this is the concerned line. My applicatioon stops here
const data = await fetch(`https://api.weather.com/v1/current.json?key=${api_key}&q=${id}`);
const current_weather = await data.json();
console.log(current_weather);
}
render() {
return (
<BrowserRouter>
<Navbar />
<SearchBar locations={this.state.locations} findCurrentWeather={this.findCurrentWeather} />
<Breadcrumb />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/weather/:id" component={Home} /> //This is the route
<Route path="/about" component={About} />
</Switch>
</BrowserRouter>
)
} // render
} // class
SearchBar functionnal component :
const SearchBar = (props) => {
const searchResult = props.locations ? (
props.locations.map((loc) => {
return (
<li className="search-item" key={loc.id}>
<Link to={"/weather/" + loc.id} onClick={props.findCurrentWeather}> //I call the function from App.js here
<span id="city">{loc.name}</span>
</Link>
</li>
) // return
})
) :
(<li className="search-item">
<Link to={"#"}>
<span>No result</span>
</Link>
</li>);
return (
<form onSubmit={props.searchLocation}>
<input type="text" name="search" id="searchInput" className="search-input"/>
<div className="search-result">
<ul className="search-list" id="searchList">
{searchResult}
</ul>
</div>
</div>
</form>
)
} //fun
export default SearchBar;
I don't know why the props in this.props.match.params.id is undefined
Your component needs the match as a props which you need to pass it. The render prop of Route component takes a function which passes props as the first argumment. The match is one of the props that the Route gives you. For example -
const App = () => (
<div>
<Header />
<Switch>
<Route exact path="/" component={Home} />
<Route path="/weather/:id" render={ ({match}) => <Home id={match.params.id}/> } /> //This is the route
<Route path="/about" component={About} />
In your example, the SearchBar needs to be inside the Home component since the id param is passed in the route to the Home component.
You may want to check this link for understanding better how the match param works in react-router.
So the thing is your page renders twice first time the id is null while second time it has the value.If you console.log(props.match) it will show you the object.
So to skip this error you can use ternery operator so your id will be equal to a value once it is loaded.Hopefully it helps.Inside findCurrentWeather function use this.
const id = props.match ? props.match.params.id : null
console.log(id);
You need to wrap your component with withRouter to be able to acceess the router from a component not rendered by a Route and move findCurrentWeather inside the SearchBar component.
// Searchbar.js
import { withRouter } from "react-router";
...
export default withRouter(SearchBar);
The problem here is the context of this in function findCurrentWeather. Since it is an arrow function it holds the instance of component where it is defined, not of the calling component. Read more about the difference here.
Now you have two options:
You shift the method definition to SearchBar component.
Make findCurrentWeather method as normal javascript function something like given below.
async function findCurrentWeather () {
let id = this.props.match.params.id; //this is the concerned line. My applicatioon stops here
const data = await fetch(`https://api.weather.com/v1/current.json?key=${api_key}&q=${id}`);
const current_weather = await data.json();
console.log(current_weather);
}

How to implement nested Routing (child routes) in react router v4?

The component tree i want is as below
- Login
- Home
- Contact
- About
Contact and About are children of Home.
This is my App.js ,
class App extends Component {
render() {
return (
<BrowserRouter>
<div>
<Route exact path="/home" component={HomeView} />
</div>
</BrowserRouter>
);
}
}
render(<App />, document.getElementById('root'));
This is Home,
export const HomeView = ({match}) => {
return(
<div>
<NavBar />
Here i want to render the contact component, (Navbar need to stay)
</div>
)
}
This is my Navbar,
export const NavBar = () => {
return (
<div>
<Link to="/home">Home</Link>
<Link to="/home/contact">Contact</Link>
<hr/>
</div>
)
}
Contact component just need to render "hello text".
To make nested routes you need to remove exact:
<Route path="/home" component={HomeRouter} />
And add some routes:
export const HomeRouter = ({match}) => {
return(
<div>
<NavBar />
{/* match.path should be equal to '/home' */}
<Switch>
<Route exact path={match.path} component={HomeView} />
<Route exact path={match.path + '/contact'} component={HomeContact} />
<Switch>
</div>
)
}
You don't need use match.path in nested routes but this way it will be easier to move everything from "/home" to "/new/home" in case you decide to change your routing.

React: Hide a Component on a specific 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.

Resources