React router v4 - Rendering two components on same route - javascript

I have these routes
<Route exact path={`/admin/caters/:id`} component={Cater} />
<Route exact path={'/admin/caters/create'} component={CreateCater} />
When I navigate to the first route I get a cater with a given ID. And the Cater component is rendered
When I navigate to the second route, the CreateCater component is rendered on the page, but I noticed that some redux actions that are used in the Cater component are being run. So both component are somehow being rendered - but I can't figure out why.
Here are the components:
Cater:
class Cater extends Component {
async componentDidMount() {
console.log('Cater component did mount')
const { match: { params: { id }}} = this.props
this.props.get(id)
}
render() {
const { cater } = this.props
if(!cater) {
return null
}
else {
return (
<div>
... component data ...
</div>
)
}
}
}
const mapStateToProps = (state, props) => {
const { match: { params: { id }}} = props
return {
cater: caterSelectors.get(state, id)
}
}
const mapDispatchToProps = (dispatch, props) => {
return {
get: (id) => dispatch(caterActions.get(id))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Cater)
CreateCater:
export default class CreateCaterPage extends Component {
render() {
return (
<React.Fragment>
<Breadcrumbs />
<CaterForm />
</React.Fragment>
)
}
}
When I go to /admin/caters/create' I can see the console.log in the componenDidMount() lifecycle method inside the Cater component.
I cant figure out what I am doing wrong :(

/create matches /:id, so it makes sense that this route matches. I recommend forcing :id to look for numeric only:
<Route exact path={`/admin/caters/:id(\\d+)`} component={Cater} />
<Route exact path={'/admin/caters/create'} component={CreateCater} />
Likewise, you can follow #jabsatz's recommendation, use a switch, and have it match the first route that matches. In this case, you would need to ensure that the /admin/caters/create route is the first <Route /> element matched.

The problem is that :id is matching with create (so, it thinks "see cater with id create"). The way to solve this is to put the wildcard matching route last, and wrapping all the <Routes/> with a <Switch/>, so it only renders the first hit.
Check out the docs if you have any more questions: https://reacttraining.com/react-router/core/api/Switch

Related

react.js & django, useParams unable to navigate to the page

I am current building a react app with django, I am trying to navigate from the HomePage to the DataPage with corrsponding id. However, it return Page not found error. I am using react-router-dom v6.
Using the URLconf defined in robot.urls, Django tried these URL patterns, in this order:
admin/
api/
api-auth/
homepage
homepage/data
The current path, homepage/data/54, didn’t match any of these.
Here is my App.js
export default class App extends Component {
constructor(props) {
super(props);
}
renderHomePage() {
return (
<HomePage />
);
}
render() {
return (
<BrowserRouter>
<Routes>
<Route exact path='homepage/' element={this.renderHomePage()} />
<Route path='homepage/data/:id' element={<DataPage />} />
</Routes>
</BrowserRouter>
)
}
}
const appDiv = document.getElementById("app");
render(<App />, appDiv);
And I want to navigate to the DataPage below:
const EmtpyGrid = theme => ({
Grid: { ... }
});
function DataPage(props) {
const { classes } = props;
const { id } = useParams();
return (
<div>
... some material ui components ...
<div/>
)
};
DataPage.propTypes = {
classes: PropTypes.object.isRequired,
};
export default withStyles(EmtpyGrid)(DataPage);
I was thinking whether I need configure my url.py in frontend as well, and I need to define a designated value for {id} returned from the materialui component first. Perhaps I need a button or <Link><Link/> for the navigation instead of just simply typing in the url? Still, no luck after many attempts. I am so confused right now.
If you know what is wrong with my code, please feel free to leave a comment. Thanks
After many tries and checking documents, I don't really need to configure my urls.py. I only things that I am missing is to put a parameter in my naviagate() from onRowClick={((rowData, event) => {navigate('data/');})} to onRowClick={((rowData, event) => {let id = event.sample_ID; navigate('data/' + id)})}; I was thinking the problem too complicated.
Thanks you guys for sharing!

Dynamically render a react component with react routing, based on value given in url

Basically what I want to do is create a react portfolio project that contains and showcases all of my react projects. But I don't know how to render a project based on a url value.
What I mean is,
<Route path='/projects/:projectName' component={Project}></Route>
I want to render a component based on the :projectName vakue.
Or maybe create a Project component that just renders the given project based on the url value.
Is that even possible? I know I can use match to get the :projectName value, but how could I use it to render a component?
There are few approaches
1. As mentioned above to let project component decide what should be rendered based on match.params
const routes = {
'my-route1': <MyComponent1 />,
'my-route2': <MyComponent2 />
}
const Project = props => {
const { projectName } = props.match.params
return routes[projectName] || <DefaultComponent />
}
You may define your own routes components who will decide which component to Render based on state. It is helpful when you need to create master pages or templates and do not want any dependencies on match inside other components.
const PrivateRoute = ({ component: Component, ...rest }) => {
const func = props => (!!rest.isUserAllowedToNavigate()
? <Component {...props} />
: (
<Redirect to={
{
pathname: '/login',
search: props.location.pathname !== '/' && queryStringComposer({
redirect_from: props.location.pathname || getQueryStringParam('redirect_from')
})
}
}
/>
)
)
return (<Route {...rest} render={func} />)
}
/* Connecting to redux */
const PrivateRouteConnected = connect(mapStateToProps, mapDispatchToProps)(PrivateRoute)
/* Using as normal routes */
<PrivateRouteConnected exact path="/dashboard" component={Dashboard} />
your Project component can handle the logic to render a different component based on the URL param. For example:
const Project = props => {
const { projectName } = props.match.params
if (projectName === project1) {
return <ProjectOne addProps={addProps} />
}
if (projectName === project2) {
return <ProjectTwo />
}
return <DefaultProject />
}

How to access 'this.props.match.params' along with other props?

I have this code in my parent component:
<Route path='/champions/:id' render={(props) => <ChampionDetails {...props} match={this.handleMatch}/>}/>
where the match prop will be a function that retrieves data from the child component (ChampionDetails). A snippet of my ChampionDetails (child) component:
import React, { Component } from 'react';
class ChampionDetails extends Component {
state = {
champId:this.props.match.params.id,
champData:null,
match:false
}
componentDidMount(){
console.log('ChampionDet mounted')
this.handleChampInfo();
console.log(this.props.match)
}
componentDidUpdate(){
console.log('ChampionDet updated')
}
handleChampInfo=()=>{
let champData = [];
let id = this.state.champId
console.log(id)
fetch('/api/getChampion?id='+id)
.then(data=> { return data.json() })
.then(json=> {
console.log(json.data);
champData.push(json.data);
this.setState({
champData:json.data,
match:true
})
this.props.match(true)
// this.props.history.push('/champions/'+id)
})
.catch(err=>{
console.log(err)
})
}
render(){
return(
<div>
<p>{this.state.champId}</p>
{this.state.champData===null ? null:<p>{this.state.champData.roles}</p>}
</div>
)
}
}
export default ChampionDetails;
The problem here is that if I have the match={} in my parent's route, then this.props.match.params.id will become undefined. If I remove match={} then I can retrieve this.props.match.params.id
I would like to know if its possible to be able to pass other props while still have access to this.props.match.params.id in my case.
Any help will be much appreciated!
If you're using react-router version >=4, you should be able to access the router params at any component inside the router via withRouter HOC. For instance:
import { withRouter } from 'react-router-dom';
...
export withRouter(ChampionDetails);
You can pass matchProps as the props from the router, this.props for any props from the parent and then just avoid overwriting props - your match prop is overriding the props from the route:
<Route path='/champions/:id' render={(matchProps) =>
<ChampionDetails
{...matchProps}
{...this.props}
handleMatch={this.handleMatch}
/>
}/>
When you spread {...props} your match prop overrides react-router's match.
Match prop is part of the react-router-dom, so by making another props called match you are overwriting it.
The simplest way: just rename the match={this.handleMatch}/>} to something else like
matchHandler={this.handleMatch}/>}
If using the name match is essential, destruct it as const {matchHandler : match} = this.props

How to pass a wrapped component into React Router without it constantly remounting

We're in the process of upgrading our React App, and after many of hours of pain have realised that passing wrapped components into React Router (V4 and maybe others) causes the component to "remount" every time a new prop is passed in.
Here's the wrapped component...
export default function preload(WrappedComponent, props) {
class Preload extends React.Component {
componentWillMount() {
getDataForComponent(props);
}
render() {
return <WrappedComponent {...props} />;
}
}
return Preload;
}
And here's how we're using it...
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit" component{preload(FlagForm, props)} />
);
};
Anytime we're dispatching an action and then receiving a update, the component remounts, causing lots of problems. According to this thread on github, components will remount if:
you call withRouter(..) during rendering which would create a new component class each time
you pass a new function to Route.component each render, e.g. using anonymous function
{...}} />, which would create a new component as well
If I pass the FlagForm component in directly the problem is fixed, but then I can't take advantage of the preload function.
So, how can I achieve the same outcome, but without the component remounting?
Thanks in advance for any help!
The reason Route is mounting a new component on every update is that it's been assigned a new class each time via preload.
Indeed, each call to preload always returns a distinct anonymous class, even
when called with the same arguments:
console.log( preload(FlagForm,props) != preload(FlagForm,props) ) // true
So, since the issue is that preload being called within the FlagsApp component's render method, start by moving it outside of that scope:
const PreloadedFlagForm = preload(FlagForm, props) //moved out
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit"
component={PreloadedFlagForm} /> //assign component directly
);
};
This way the component for Route won't change between updates.
Now about that lingering props argument for preload: this is actually an anti-pattern. The proper way to pass in props just the standard way you would for any component:
const PreloadedFlagForm = preload(FlagForm) //drop the props arg
const FlagsApp = (props) => {
return (
<Route path="/report/:reportId/flag/:id/edit"
component={<PreloadedFlagForm {...props} />} //spread it in here instead
/>
);
};
And so the code for preload becomes:
export default function preload(WrappedComponent) {
class Preload extends React.Component {
componentWillMount() {
getDataForComponent(this.props);
}
render() {
return <WrappedComponent {...this.props} />;
}
}
return Preload;
}
Hope that helps!
If like me you didn't read the instructions, the answer lies in the render prop of the <Route> component
https://reacttraining.com/react-router/web/api/Route/render-func
render: func
This allows for convenient inline rendering and wrapping without the undesired remounting explained above.
So, instead of passing the wrapper function into the component prop, you must use the render prop. However, you can't pass in a wrapped component like I did above. I still don't completely understand what's going on, but to ensure params are passed down correctly, this was my solution.
My Preload wrapper function is now a React component that renders a Route...
export default class PreloadRoute extends React.Component {
static propTypes = {
preload: PropTypes.func.isRequired,
data: PropTypes.shape().isRequired,
location: PropTypes.shape({
pathname: PropTypes.string.isRequired,
}),
}
componentWillMount() {
this.props.preload(this.props.data);
}
componentWillReceiveProps({ location = {}, preload, data }) {
const { location: prevLocation = {} } = this.props;
if (prevLocation.pathname !== location.pathname) {
preload(data);
}
}
render() {
return (
<Route {...this.props} />
);
}
}
And then I use it like so...
const FlagsApp = (props) => {
return (
<Switch>
<PreloadRoute exact path="/report/:reportId/flag/new" preload={showNewFlagForm} data={props} render={() => <FlagForm />} />
<PreloadRoute exact path="/report/:reportId/flag/:id" preload={showFlag} data={props} render={() => <ViewFlag />} />
<PreloadRoute path="/report/:reportId/flag/:id/edit" preload={showEditFlagForm} data={props} render={() => <FlagForm />} />
</Switch>
);
};
The reason I'm calling this.props.preload both in componentWillMount and componentWillReceiveProps is because I then had the opposite issue of the PreloadRoute component not remounting when navigating, so this solves that.
Hopefully this save lots of people lots of time, as I've literally spent days getting this working just right. That's the cost of being bleeding edge I guess!

React Router redirect hash link

I have created a custom button component for my website's navbar. When the user clicks on a button, the component returns a Redirect, which takes the user to the page they selected.
export default class Button extends Component {
constructor(props){
super(props);
this.state = {redirect:false};
this._handleClick = this._handleClick.bind(this);
}
_handleClick(e) {
e.stopPropagation();
this.setState({redirect: true});
}
componentDidUpdate() {
if (this.state.redirect){
this.setState({redirect:false});
this.props.onRedirect();
}
}
render() {
if (this.state.redirect){
return <Redirect push to={this.props.dest}/>;
}
else {
return (
<li className="button" onClick={this._handleClick}>
<h5>{this.props.text}</h5>
</li>
);
}
}
}
Now, I'd like to add buttons that correspond to different sections of the same page. The simplest way I know of is to use hash links. One example of an address the button would redirect to is:
/home#description
However, React Router does not support doing this out of the box. I looked through a number of packages which add this functionality, such as react-router-hash-link and react-scrollchor. None of these however work with redirects, instead relying on Link or on custom components.
How do I go about adding this functionality to the buttons?
you could update window.location.href since it won't trigger a page refresh.
e.g.
window.location.href = '#your-anchor-tag';
One solution that I can think of is to use HOCs and hooks. The end result:
You'll get your app to scroll to the specified location...
without really needing to create custom buttons/links and...
without making much changes to your existing screens (Eg: HomeScreen)
Bonus: Users can copy, share & use URLs that will automatically scroll to the intended section
With assumption that the code below are pseudocode (they are based on my knowledge and not tested) and assuming there's a HomeScreen component, I would attempt adding <Route/>s to the <Switch/> inside the <Router/>.
<Switch>
<Route to='/home/:section' component={HomeScreen} />
<Route to='/home' component={HomeScreen} />
</Switch>
Then:
function withScrollToTarget(WrappedComponent) {
class WithScroll extends React.Component {
componentDidMount() {
const { match: { params: { section } } } = this.props
// Remember we had 2 <Route/>s, so if `section` is provided...
if (section) {
const scrollToTarget = document.getElementById(section)
// And just in case the item was removed or there was an ID mismatch
if (scrollToTarget) { scrollToTarget.scrollIntoView() }
}
}
render() { return <WrappedComponent {...this.props} /> }
}
return WithScroll
}
function useScrollToTarget(section) {
useEffect(() => {
if (section) {
const scrollToTarget = document.getElementById(section)
if (scrollToTarget) { scrollToTarget.scrollIntoView() }
}
}, [section])
}
Usage:
<nav>
<Link to='/home'>{'Home'}</Link>
<Link to='/home/description'>{'Description'}</Link>
</nav>
class HomeScreen extends React.Component { /* ... */ }
export default withScrollToTarget(HomeScreen)
// or
function HomeScreen() {
const { params: { section } } = useMatch() // from react-router-dom
useScrollTotarget(section)
return (
<div>
<h1 id='introduction'>Introduction</h1>
<h1 id='description'>Description</h1>
</div>
)
}
TLDR:
The route for '/home/:section' must be on top of '/home'. If the opposite, every time when <Switch/> compares the current URL against to, it will evaluate to true upon reaching '/home' and never reach '/home/:section'
scrollIntoView() is a legit function
If this works for you, you should look up on how to forward refs and hoisting statics in HOCs too
Who said React Router doesn't support this out of the box! You don't need those packages. You can redirect a hash i'll give you an example using the React-Router Route.
<Route
exact
path="/signup"
render={props => {
if (props.location.hash === "#foo")
return <Redirect push to="signup#bar"
return <Signup />
}}
/>
Now your version may not have supported this now that I think about it, but let me know if this helps :)
Happy coding!
React-hash-link should work for your redirect use case.
You can add <HashLinkObserver /> to your component tree and it will listen for hash links and scroll accordingly rather than relying on Link or custom components.
I think you should use the react-router-dom.
yarn add react-router-dom
Now update Custom Button Component like this
import React from 'react';
import { withRouter } from "react-router-dom";
class Button extends Component {
constructor(props){
super(props);
this.state = {redirect:false};
this._handleClick = this._handleClick.bind(this);
}
_handleClick(e) {
e.stopPropagation();
this.setState({redirect: true});
}
componentDidUpdate() {
if (this.state.redirect){
this.setState({redirect:false});
//this.props.onRedirect();
this.props.history.push('new uri');
}
}
render() {
if (this.state.redirect){
return <Redirect push to={this.props.dest}/>;
}
else {
return (
<li className="button" onClick={this._handleClick}>
<h5>{this.props.text}</h5>
</li>
);
}
}
}
export default withRouter(Button);
I was trying to solve a similar but slightly different issue, I want to deprecate an old hash route in favor of a new one. The posts here helped me arrive to my eventual solution:
<Route
exact
path={'/thing/:id'}
render={({
match: {
params: { id },
},
}) => (
<Redirect
push
to={`/newThing/${id}`}
/>
)}
/>
I was facing the same issue, I have created HOC to handle hash redirection, you can follow the below steps to achieve a hash redirection
create HOC and add below code to it
fileName : hashComponent
import React, { useEffect } from 'react';
export default function hashComponent(WrappedComponent) {
return function () {
const { pathname, hash }=window.location;
useEffect(() => {
if(hash)
window.location.href=`${pathname}${hash}`;
}, [hash])
return <WrappedComponent />
}
}
import your HOC in the component to which you want to handle hash URL
Then add below line of code while exporting your component
export default hashComponent(YourComponentName)

Categories

Resources