Usage of nested Route element - reactjs

In my React app, I can see the following code;
const pages = [
{
pageLink: '/state/:stateCode',
view: State,
displayName: t('State'),
animationDelayForNavbar: 0.7,
showInNavbar: false,
},
];
<Route render={({location}) => (
<div className="Almighty-Router">
<Navbar
pages={pages}
darkMode={darkMode}
setDarkMode={setDarkMode}
/>
<Switch location={location}>
{pages.map((page, index) => {
return (
<Route
exact
path={page.pageLink}
render={({match}) => (
<page.view key={match.params.stateCode || index} />
)}
key={index}
/>
);
})}
<Redirect to="/" />
</Switch>
</div>
)}
/>
My question is why is there a nested <Route> used below ?
What purpose does it serve? Can we somehow implement this without nested <Route> element ?

It's because your component might have been developed sometime back.
The recommended method of rendering something with a <Route> is to use children elements. There are, however, a few other methods you can use to render something with a <Route>. These are provided mostly for supporting apps that were built with earlier versions of the router before hooks were introduced.
Instead of having a new React element created for you using the component prop, you can pass in a function to be called when the location matches. The render prop function has access to all the same route props (match, location and history) as the component render prop.

You do not need to render a nested route, specially because your Parent route is not defining any path.
You can directly render he above component like below
<div className="Almighty-Router">
<Navbar
pages={pages}
darkMode={darkMode}
setDarkMode={setDarkMode}
/>
<Switch>
{pages.map((page, index) => {
return (
<Route
exact
path={page.pageLink}
render={({match}) => (
<page.view key={match.params.stateCode || index} />
)}
key={index}
/>
);
})}
<Redirect to="/" />
</Switch>
</div>
Also the Switch component doesn't need a location prop

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.

Why is the component render method called before unmount?

I have 2 routes which render the same component.
Once I am on route path "/route1", I click on a link which routes me to /route2. I expect componentWillUnmount() to be called but instead the render method is called again with prop type "type2". Why is this happening?
I have already tried adding a key to each Route but it does not work.
`<Switch>
<Route
path={'/route1'}
render={props => (
<Component1
type={type1}
{...props}
/>
)}
/>
<Route
path={'/route2'}
render={props => (
<Component1
type={type2}
{...props}
/>
)}
/>
</Switch>`

React Router 4 - accessing child params

I need to get access to the params (e.g. match.params) for a child component. For example:
const Vets = ({ match }) => (
<div>
<LeftMenu vetId={match.params.vetId} catId=??? />
<div>
<Switch>
<Route path={`${match.path}/cats/:catId`} component={Cat} />
<Route path={`${match.path}/dogs/:dogId`} component={Dog} />
</Switch>
</div>
<RightActionBar /> (<- uses withRouter to get vetId. Would like to get catId)
</div>
)
URL: myapp.com/vets/12/cats/194
In the Vet component I can access match.params and get any params that were in the section of the URL that matched in the parent to present this component (such as vetId, but not catId). Also, if I use withRouter in the RightActionBar component, I can get the same match.params, BUT NOT THE CAT OR DOG ID.
In the Cat and Dog components, I can access props.match.params and get the params available above AND THE CAT OR DOG ID.
Lets say I want to know the cat or dog id in the LeftMenu or RightActionBar component. How would I access it?
As far as I can tell, this was doable in earlier versions through props.params, but not any more. I know I could use the location prop and write my own function to work it out, but my app is a little more complex than the example and I'm hoping theres a more elegant solution out of the box that I've missed.
I ended up solving this by creating a component that rendered my LeftMenu and RightActionBar:
const Wrapper = ({ animalType , match: { params: { animalId, vetId }}}) => (
<>
<LeftMenu animalId={animalId} animalType={animalType} vetId={vetId} />
{children}
<RightActionBar vetId={vetId} />
</>
)
Then I used the router render method instead of component:
const Vets = ({ match }) => (
<div>
<Switch>
<Route
path={`${match.path}/cats/:animalId`}
render={(props) => (
<Wrapper animalType={'cat'} {...props}>
<Cat catId={props.match.params.animalId} />
</Wrapper>
)}
</Route>
<Route
path={`${match.path}/dogs/:animalId`}
render={(props) => (
<Wrapper animalType={'cat'} {...props}>
<Dog dogId={props.match.params.animalId} />
</Wrapper>
)}
</Route>
</Switch>
</div>
)
I could also have just added the <Wrapper/> component to the outer layer of the <Cat/> and <Dog/> renders instead, but I wanted to keep them clean so I could use them without it elsewhere

React Router v4 dynamic nested routes do not match

I ask this question just want to make sure that I understand the dynamic nested routes in React Router v4 correctly. I want to build a hacker news client similar to this one. But I am bogged down by setting up the dynamic routes.
In React Router v4, if I follow other tutorials on the web using the match object I will have something like this (A super simple one):
const ChildComponent = ({rows, match}) => (
<div>
<ol>
rows.map((row, i) => {
return (
<li key={row.id}>
<a href={row.url}>row.title</a> // topic title
<Link to=`${match.url}/${row.by}`>row.by</Link> // topic author
</li>
)
}
</ol>
<Route path=`${match.url}/:userId` render={(props) => <UserProfile fetchUserData={fetchUserData} {...props} />} />
</div>
)};
And when we render the parent component, we usually use something like this for routing:
<Switch>
<Route path="/" render={Home} />
<Route path="/topics" render={(props) => <ChildComponent rows={rows} {...props} /> } />
<Route path="*" render={() => <div>Not Found</div>} />
</Switch>
But this is not ideal for this case, as when I click to view the author's info I need to display a url like this: "http://mysite/user/userid" instead of the current one which is "http://mysite/news/userid".
However, if I change ${match.url}/${row.by} to /user/${row.by} and change ${match.url}/:userId to /user/:userId the route is not recognized in the app. The route begins with /user/ is simply skipped, it will go straight to the app's NotFound route (if there is one), which is in the parent component. Why will the links in child component try to match the routes in the parent if I don't use ${match.url} in the route?
I have added a demo for you to easier to understand this problem.
Because when you'll click the /user/:userId: link the app will parse the Router's Switch to see if something matches. If it doesn't it fallback to *.
In your case, you did not specify anything in the Switch to handle /user .
You'll need to move your userId Route declaration to the Switch as they won't share the same first route (/user !== /topics).
<Switch>
<Route path="/" render={Home} />
<Route path="/topics" render={(props) => <ChildComponent rows={rows} {...props} /> } />
<Route path="/user/:userId" render={(props) => <UserProfile fetchUserData={fetchUserData} {...props} />} />
<Route render={() => <div>Not Found</div>} />
</Switch>

React set state/props on route using react router

I am somewhat new to React. so please bear with me. I have the following base structure:
<App>
this.props.children
</App>
...and in children, one component is a header that has what I want to be an optional search component:
<Header>
...some other children...
<Search /> <- STUCK HERE SHOULD BE OPTIONAL!!!
</Header>
...children from other components...
What I am trying to do is say when I go to route A, the search component should not be included (or at least not shown), but when I go to route B, it should. I have scoured for days and so far been unable to find a solution that meets this need. If it matter, I am using ES6/7 (babel via webpack).
I can set state in the APP and toggle it literally to adjust the passed down props on the Search and show or not show it, but cannot figure out how to do that dynamically based on the route.
The core issue is how to tell App (and indirectly Header) to show the search component on inside the Header on some routes, but not on others. I 'think' maybe I need some sort of abstraction/wrapper component in the middle, but am not really sure. Any though or ideas are welcome.
TIA!
First setup your routes.
<Router path="/" component={App}>
<Route path="foo" component={Header} showSearch={true} />
<Route path="bar" component={Header} showSearch={false} />
</Router>
The route will be passed down as a property, then you can access the showSearch property, which determines whether the search component is rendered.
// Header
render() {
const { showSearch } = this.props.route;
return (
<div className='header'>
// ... other components
{ showSearch ? <Search /> : null }
</div>
);
}
Maybe you don't want your header to be the top level component though. In that case define an intermediary wrapper component that forwards the route props down to the header.
<Router path="/" component={App}>
<Route path="foo" component={Shell} showSearch={true} />
<Route path="bar" component={Shell} showSearch={false} />
</Router>
// Shell
render() {
const { route } = this.props;
return (
<div className='shell'>
<Header {...route} />
</div>
);
}
Alternatively, you could do a quick and dirty check from inside your Header component.
// Header
render() {
const { hash } = window.location,
showSearch = /\/foo/.test(hash);
return (
<div className='header'>
// ... other components
{ showSearch ? <Search /> : null }
</div>
);
}
If you want to use functional components, React Router has specifically created an API for this called render for this purpose.
Example:
<Route
path='/search'
render={(props) => (
<Header {...props} showSearch={true} />
)}
/>
Then just simply use the props as normal in your component:
interface HeaderProps {
showSearch: boolean
}
export const Header: React.FC<HeaderProps> = ({ showSearch }) => {
return (
<React.Fragment>
{ showSearch ? <Search /> : null }
</React.Fragment>
)
}
See the excellent article written by Tyler McGinnis regarding this implementation:
https://ui.dev/react-router-v4-pass-props-to-components/

Resources