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
Related
I'm new to react. My app has array of restaurants with different categories. User should be able to go to page which loads all restaurants by categories or be able to go to restaurant's page.
Path /eat-out loads all available categories with a few suggestions for restaurants.
Category page works fine, but when I try to enter restaurant page it still loads category component instead of restaurant's.
I need to have a path eat-out/categories or eat-out/restaurant, where each would load a different component.
return (
<content className="content">
<Switch>
<Route exact path="/" render={() => <Dashboard />} />
<Route exact path="/reservations" render={() => <Reservations />} />
<Route exact path="/eat-out" render={() => <EatOut />} />
<Route
exact
path="/eat-out/:category"
render={() => <CategoryRestaurants />}
/>
<Route
exact
path="/eat-out/:restaurant"
render={() => <RestaurantPage />}
/>
</Switch>
</content>
);
};
Is it possible to do such routing?
This is possible if you know what the categories and/or restaurants are, but probably not what you really want:
import React from "react";
import { Route, BrowserRouter as Router, Switch } from "react-router-dom";
import "./styles.css";
const CategoryRestaurants = ({ category }) => <div>Category: {category}</div>;
const RestaurantPage = ({ restaurant }) => <div>Restaurant: {restaurant}</div>;
const KNOWN_CATEGORIES = ["cat1", "cat2", "cat3"];
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<Router>
<Switch>
<Route
exact
path="/eat-out/:category"
render={(props) => {
// Use the list of known categories to decide where to go
const param = props.match.params.category;
return KNOWN_CATEGORIES.includes(param) ? (
<CategoryRestaurants category={param} />
) : (
<RestaurantPage restaurant={param} />
);
}}
/>
</Switch>
</Router>
</div>
);
}
It would likely be better to have more defined URLs, such as /eat-out/category/:category and /eat-out/restaurant/:restaurant.
There are a few approaches here that might work for you.
Pattern Matching
If the values for category and restaurant have different patterns that you can check with regex, you can use pattern matching for your URL parameters, like so:
path="/eat-out/:category([A-Za-z]+)"
path="/eat-out/:restaurant([0-9]+)"
Static Value Matching
Or if you know all the values for category and restaurant ahead of time (i.e. the values are static and not pulled from the server/database), you can create a list like this and use regex:
const categories = ["Pizza", "Hamburgers", "Vegan"];
const categoryPattern = categories.join("|");
const restaurants = ["Tom's Sandwiches", "Super Restaurant Co."];
const restaurantPattern = restaurants.join("|");
path={`/eat-out/:category(${categoryPattern})`}
path={`/eat-out/:restaurant(${restaurantPattern})`}
Modify the Paths
If the values cannot be split by pattern and you don't know the values ahead of time and you don't mind changing the paths a bit, you can add a path before the URL parameter to tell the difference, like this:
path="/eat-out/categories/:category"
path="/eat-out/restaurants/:restaurant"
Internal Component Logic
Otherwise, you can create a single component that uses more complex logic to tell the difference between a category and a restaurant internally, then return either <CategoryRestaurants> or <RestaurantPage> depending on which is relevant.
For example you can try fetching category data based on the name/ID and if that fails, then fetch restaurant data instead. Alternatively, you can make a single route in your API that accepts a name/ID and returns the correct object, then just figure out what object type you received on the front-end and render accordingly.
So you would have something like this:
<Route
path="/eat-out/:categoryOrRestaurant"
render={() => <CategoryOrRestaurantPage/>}
/>
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
I have an issue with react router 4. And I'm not sure if its solvable:
This is my application layout:
Which basically works. But the problem is that whenever I hit /items/:id/ via the link on the left side it also matches /items. Which causes the link list in the sidebar to rerender. The solution would be to nest the routes. But this is not possible due to the interface/DOM. The left sidebar needs be independent of the item detail. And I need to split those up like:
<div>
<div className={styles.sidebar}>
<HeaderContainer />
<Switch location={location}>
<Route exact path="/" component={Homepage} />
<Route path="/items" component={Items} />
</Switch>
</div>
<div className={styles.content}>
<Route path="/items/:id" component={ItemDetail} />
</div>
</div>
Thanks a lot for your help in advance!
I had a similar layout and I used something like this
//App.jsx
<Router path="/" component={Page}>
//Page.jsx
<Layout>
<MasterView>
<DetailView>
</Layout>
//MasterView.jsx
componentDidMount() {
const { dispatch } = this.props
const data = await api.getData();
dispatch(updateDetail(data));
}
connect()(MasterView)
// DetailView.jsx
import { connect } from 'react-redux';
render() {
return <ul>{this.props.list.map((item) => <li>{item}</li>)}</ul>;
}
// map the props you need to redux state
const mapStateToProps = (state) => ({ list: state.data.list });
connect(mapStateToProps)(DetailView)
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
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/