How to access react router match object from mapstatetoprops - javascript

I was wondering how I would access the react-router match object for its params from mapStateToProps or any such selector. I wanted to build something out of the params and pass that down as props to the presentational component within the selector. I have a component that accepts a built prop and I'm hoping to pass it some value that's derived from the react router params. I'd preferably would like to not have to pass params down as a prop.

mapStateToProps takes second parameter ownProps. Once your component is initialized by react-router you gets match prop among your others.
<Route path="/details/:id" component={DetailsPage}
then
function DetailsPageComp(props) {
...
}
function mapStateToProps(state, { match: { params: {id} } }) {
return {
obj: someSelectorFunction(state, id);
};
}
export default connect(mapStateToProps)(DetailsPageComp);
Sure if you use <Route render={} version you need to pass match down:
<Route render={({ match }) => <DetailsPageComp match={match} />} path="..." />

Related

React Router: strangely matched Route path props passed to its wrapper

According to React Router Doc, you can spread routeProps to make them available to your rendered Component, code below works as I expected: users will see Test if entering /test, and 404 for /test2.
If I remove {...rest}, I expected users will always see Test because the Route without a path should always match. But it still behaves the same. Can anyone tell me what's going on here?
function RouteWrapper({ children, ...rest }) {
return (
<Route
{...rest}
render={() => children}
/>
);
}
export default function App() {
return (
<Router>
<Switch>
<RouteWrapper path="/test">Test</RouteWrapper>
<Route>404</Route>
</Switch>
</Router>
);
}
CodesandBox Demo
Interesting.
It appears to be because of how the Switch component is implemented. The following snippet from the code is of particular interest:
let element, match;
React.Children.forEach(this.props.children, child => {
if (match == null && React.isValidElement(child)) {
element = child;
const path = child.props.path || child.props.from;
match = path
? matchPath(location.pathname, { ...child.props, path })
: context.match;
}
});
As you can see, the component simily looks through its children components and finds the path prop. It assumes you've passed a proper path prop to each Route component.
So it doesn't matter that you removed the path from your Route, because the Switch will take the path from your RouteWrapper component.

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

React router v4 - Rendering two components on same route

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

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!

Categories

Resources