React Router 4 - accessing child params - reactjs

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

Related

React router Link doesn't redirect

Please see sandbox.
I'm having this code:
<Router history={createBrowserHistory()}>
<Switch>
<Route path="/items/:itemId">
<Item />
</Route>
<Route exact path="/items">
<Items />
</Route>
<Redirect to="/items" />
</Switch>
</Router>
Expecting /items to be default route. Items is loading links for available items:
const { url } = useRouteMatch();
return (
<div>
<h1>Items</h1>
{items.map((item) => (
<Link to={`${url}/${item}`}>{item}</Link>
))}
</div>
);
Once an item link is clicked, I'm expecting the url to have the item in it, and to render Item.
url is /items, so everything should work out for Switch to render:
<Route path="/items/:itemId">
<Item />
</Route>
However, it's staying at /items, and not even rendering Items component anymore. No error is shown. Any idea why this is happening?
For router you are missing an exact props which is really important for your case
<Route exact path="/items/:itemId" />
and inside your Item component you misspelled the param name, because in router it's itemId where the component has a different one item, so fixing it will make the param accessible(they should be exact match)
export const Item = () => {
const { itemId } = useParams();
return (
<div>
<h1>Item</h1>
{itemId}
</div>
);
};
That because when it reaches the /items/:itemId Page the component get redirect back to /items , So the solution to this is just remove the redirect component
<Switch>
<Route path="/items/:itemId">
<Item />
</Route>
<Route exact path="/items">
<Items />
</Route>
</Switch>
Also there few other mistake which i saw in your code.In Item.jsx , your using
const { item } = useParams();
This isn't going to the render the anything since it is undefined , that is because your are passing the /:itemId in Route. So try this instead
const { itemId:item } = useParams();
here we are renaming the itemId to item inorder to accessable down in the code.

Usage of nested Route element

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

React passing props down through switch element

I'm having an issue passing props through React elements (like Switch and Route). In the example below, I would like to pass all props of Dashboard component down to the Account component. Is there a way to achieve this?
App.js
<Dashboard>
<Switch>
// Dashboard props to Account component
<Route path="/account" render={props => <Account {...props} /> } exact />
<Route path="/someothercomponent" component={Someothercomponent} />
</Switch>
</Dashboard>
Dashboard.js
render() {
const children = React.Children.map(this.props.children, child => {
var router = React.cloneElement(child, { image: this.state.image });
return router;
// Like this the router Element does receive the image prop from
// the Dashboard component. Now this image prop needs to be
// passed on to the Account component.
}
I like some of the answers already present. To give you a sense of solving this problem differently and also something to learn and add to your toolbox. I would say use Context. Context provides a way to pass data through the component tree without having to pass props down manually at every level. https://reactjs.org/docs/context.html
So if you get to your Account and have to yet again pass props down this might be a good place to implement this.
When setting up correctly you could do something like this on your page. But again you aren't just passing down one you are passing down all props. And then what if you need to also pass them down on the next component <<< this is the point of Context. I would think using context is better than using a component as your state considering a stateful component is usually limited. With context, your Account component could have several children and you wouldn't have to pass props all the way down to get done what you wish to achieve.
<AppContext.Consumer>
{({prop1, prop2, prop3}) => {
}}
</AppContext.Consumer>
That's assuming you name your variable AppContext when you use React.createContext();
The idea is that passing down props at many levels can be annoying for some but using context you can bring a property in at any time without having to worry about if you passed them down correctly. Be sure to read the article in full there are times where you want to use context and times where you do not.
Yes, use render property instead.
<Route path="path" render={() => <MyComponent {...this.props} />} />
The problem is component is overriding the render props.
Remove component={Account}
I've also added brackets around (props) to improve readability
<Dashboard>
<Switch>
<Route
path="/account"
render={(props) => <Account {...props} /> }
exact
/>
<Route
path="/someothercomponent"
component={SomeOtherComponent}
/>
</Switch>
</Dashboard>
Alternatively:
const renderMergedProps = (component, ...rest) => {
const finalProps = Object.assign({}, ...rest);
return( React.createElement(component, finalProps)
);
}
const PropsRoute = ({ component, ...rest }) => {
return (
<Route {...rest} render={routeProps => {
return renderMergedProps(component, routeProps, rest);
}}/>
);
}
<Router>
<Switch>
<PropsRoute path='/login' component={Login} auth={auth} authenticatedRedirect="/" />
<PropsRoute path='/trades' component={Trades} user={user} />
</Switch>
</Router>
source

React Router 4 Pass props to dynamic component

I have a list of doctors and I am trying to dynamically render a details page when selected. I see most people recommend to pass props through the Route component, something like this:
<Route path={`${match.url}/:name`}
component={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
Though I'm not clear on where I should be executing this. I tried it in DoctorList and DoctorItem but that didn't work. So I've set the Route in the App component, and I am able select a doctor, which then renders the DoctorView component and display the match.params prop just fine. But how do I get the selected doctor data to DoctorView? I'm probably making this harder than it should be. Here is my code:
App.jsx
const App = () => {
return (
<div>
<NavigationBar />
<FlashMessagesList />
<Switch>
<Route exact path="/" component={Greeting} />
<Route path="/signup" component={SignupPage} />
<Route path="/login" component={LoginPage} />
<Route path="/content" component={requireAuth(ShareContentPage)} />
<Route path="/doctors" component={requireAuth(Doctors)} />
<Route path="/doctor/:name" component={requireAuth(DoctorView)} />
</Switch>
</div>
);
}
DoctorList.jsx
class DoctorList extends React.Component {
render() {
const { doctors } = this.props;
const linkList = doctors.map((doctor, index) => {
return (
<DoctorItem doctor={doctor} key={index} />
);
});
return (
<div>
<h3>Doctor List</h3>
<ul>{linkList}</ul>
</div>
);
}
}
DoctorItem.jsx
const DoctorItem = ({ doctor, match }) => (
<div>
<Link
to={{ pathname:`/doctor/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
{doctor.profile.first_name} {doctor.profile.last_name}
</Link>
</div>
);
DoctorView.jsx
const DoctorItem = ({ doctor, match }) => (
<div>
<Link
to={{ pathname:`/doctor/${doctor.profile.first_name}-${doctor.profile.last_name}` }}>
{doctor.profile.first_name} {doctor.profile.last_name}
</Link>
</div>
);
I have access to the list of doctors via Redux, I could connect the component, bring in the list and compare id’s but that feels like a lot of unnecessary steps.
But how do I get the selected doctor data to DoctorView?
Keep in mind that having paths like /items and /items/:id creates a scenario where you might be landing on the details page first.
Do you:
a) fetch all the items anyways because you might go back to the list page?
b) just fetch that the information for that one item?
Neither answer is "correct" but at the end of the day you have three possible pieces of information:
1) the item id
2) a single item
3) a list of items (which may or may not contain all of the information you need for the details page)
Wherever you want to display the full details of an item, it needs to have access to that item via props. Putting all of the item details in the url would be arduous, plus it would make it impossible to do situation A.
Since you're using redux, it makes perfect sense grab the details of the item from the identifier in the url
export default
connect((state, props) => ({
doctor: state.doctorList.find(doctor =>
doctor.id === props.match.params.id
)
}))(DoctorView)
Does ^ seems like too many extra steps?
While the answer above solves the issue perfectly, I just want to add that using an inline function with component is not advised by react-router
Instead of doing:
<Route path={`${match.url}/:name`}
component={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
You should instead use it like:
<Route path={`${match.url}/:name`}
render={ (props) => <DoctorView doctor={this.props.doctors} {...props} />}
/>
This will prevent the same component from being created on every mount and instead use the same component and update the state accordingly.
Hope this will help someone

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