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/
Related
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
Any help would be appreciated, So i have a page with Header, Sidebar, Footer and Main, where Sidebar has static links, which when clicked display the components. The issue here is on clicking the links, sidebar,header and footer are re-rendering which is not required. I have tried shouldComponentUpdate in sidebar but it won't work.
Versions used by the project:
"react": "^16.12.0",
"react-dom": "^16.12.0",
"react-router-dom": "^5.1.2",
I'll be here till this issue is resolved so feel free to ask any question
here is myApp.js (the root file)
function App() {
return (
<Provider store={Store}>
<Router history={history}>
<AppRoutes />
</Router>
</Provider>
);
}
now the AppRoutes component has following method
const RouteList = [
{
path: "/",
component: Dashboard,
guest: false,
exact: true
},
{
path: "/security_info",
component: SecurityInfoPage,
guest: false,
exact: true
},
]
class AppRoutes extends Component {
componentDidMount() {
...here we fetch the login info from store
isAuthenticated = true
}
render() {
...if it has access token, it
return (
<Switch>
{RouteList.map((route, i) => (
route.guest === false
? <PrivateRoute isAuthenticated={isAuthenticated} key={i} {...route} />
: <AppRoute key={i} {...route} />
)
)}
</Switch>
);
}
}
as is_authenticated is true, it goes to private route inside AppRoute.js file
const PrivateRoute = ({isAuthenticated, component: Component, ...rest }) => (
<Route
{...rest}
render={(props) => (
isAuthenticated === true
? <DashboardLayout>
<Component {...props}/>
</DashboardLayout>
: <Redirect to='/login' />
)}
/>
)
it goes to dashboardlayout where it has multiple components
<div className={'wrapper'}>
<Navigation />
<div className="page-content">
<Sidebar />
<div className="content-wrapper">
{children}
<MessageSideBar />
<Footer />
</div>
</div>
</div>
Now as i click on a different link, it goes to dashboard layout where its prop children gets changed rendering the entire dashboard including header, footer, sidebar.
Edit 1:
Here is the Sidebar file
class Sidebar extends Component {
componentDidMount = () => {
it is requesting data from 3 api's
this.props.dispatch(sidebarAction.sidebarDetail())
this.props.dispatch(settingAction.getCreditAmount())
this.props.dispatch(messageAction.getUnReadMessageCount())
}
render(){
return(
<ul>
<li>
<NavLink
exact={true}
to="/" >
<span>Dashboard</span>
</NavLink>
</li>
<li>
<NavLink to="/security_info">
<span>Security Information</span>
</NavLink>
</li>
</ul>
)}
Though there are like 10+ NavLinks but i have included only 2 and also removed irrelevant classnames
Incorrect way
Your routes are structured like the following snippet, which will cause rerendering the Dashboard component every time you switch the route.
<Route path="/subComponent" component={SubComponent}/>
const SubComponent = () => {
return (
<Dashboard>
// Here is your content place
</Dashboard>
)
}
Correct way
However, the correct solution would be to put the routes directly inside the Dashboard component where you would like to render your components wrapped by the layout (Dashboard) like this:
<Dashboard>
<DashboardMenu>
<SideNav /> // e.g. containing links with history.push()
</DashboardMenu>
// here is the place where the components wrapped by the layout should be rendered, so place the routes here
<Route path={...} component={() => <ComponentInsideDashboard />}
</Dashboard>
You should render the actual content (the dynamic one, not the static Dashboard) already inside the Dashboard. Since every route returns the dynamic components wrapped inside Dashboard, the Dashboard will get rendered multiple times.
To put it even more simply: Since you want the Dashboard to get rendered only once, there should just be only one place where you are using it.
Correct way with different layouts
If you also want to render content without a layout (Dashboard) or multiple different ones, you can simply make use of nesting routes.
export const BaseApplicationRoutes = () => {
return (
<BrowserRouter>
<Switch>
<Route path="/dashboard1" component={<Dashboard1 />}/>
<Route path="/dashboard2" component={<Dashboard2 />}/>
<Route path="/noDashboard" component={<NoDashboard />}/>
</Switch>
</BrowserRouter>
)
}
<Dashboard1> // Dashboard2 could also look like this
<Dashboard1Menu>
<SideNav /> // e.g. containing links with history.push()
</Dashboard1Menu>
// Routes containing dynamic content
<Route path={...} component={() => <ComponentWithDashboard1 />}
</Dashboard1>
<NoDashboard>
// any content or even more routes rendering content without a layout
</NoDashboard>
I have two different components in React "Header" and "Main".
Header:
import React, { Component } from 'react'
import Logo from './HeaderComps/Logo'
import UserHeader from './HeaderComps/UserHeader'
export default class Header extends Component {
render() {
return (
<header>
<Logo />
<UserHeader name ="Boris"/>
</header>
)
}
}
Main:
export default class Main extends Component {
state ={isLogged : false}
handleClientFirstImpact = () =>{
if(this.state.isLogged === false){
return <Switch>
<Route exact path ='/registration' component={Register} />
<Route exact path ='/login' component={Login} />
</Switch>
}
}
render() {
return (
<Router>
<div className="Main">
{this.handleClientFirstImpact()}
</div>
</Router>
)
}
}
In "Main" I have two components "Register" and "Login".
How do I make Login page effect the Header's state? is there a React technology or a way to do that?
I want the "UserHeader" to show the User name but I don't know how to effect it's parent's state.
There might be some common component where you will be calling the Main as well as the Header Component. Suppose that component is App Component.
So inside App you have
render() {
return
<div>
<Header />
<Main />
</div>
}
Now what you can do is keep the userName in this App Component's state and this App Component will pass userName to the Component as :-
<Header userName={userName} />
Also pass a function as a prop to the Main Component which will enable the component to set the State of the Parent Component.
<Main setUserName={(newUserName) => this.setState(newUserName)} />
now this setUserName prop should be passed on to the components which are called via Route inside the Main Component. Keeping your example in mind (use render prop instead of component for Route):
export default class Main extends Component {
state ={isLogged : false}
handleClientFirstImpact = () =>{
const { setUserName } =this.props;
if(this.state.isLogged === false){
return
<Switch>
<Route exact path ='/registration'
render={(props) => <Register {...props} setUserName={setUserName} />}
/>
<Route exact path ='/login'
render={(props) => <Login {...props} setUserName={setUserName} />}
/>
</Switch>
}
}
render() {
return (
<Router>
<div className="Main">
{this.handleClientFirstImpact()}
</div>
</Router>
)
}
}
Now you have passed setUserName as a prop to both login and register and you can use this method to set App component's state which will in turn reflect the changes on the Header component.
Although the solution might work for you. I would advise you to simplify the Application layout. Keep the routing functionality in the main app Component. Use a separate layout component to render similar page layouts. It would avoid confusion in the long run.
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 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