I have 2 routes using same component (form), one to create new entry and the other to edit. State name has been removed from React Router, I can't just check if this.route.name === 'edit' => fetch(itemAPI).
One option is to write reducer/action to create "edit" state in my redux store, but I was wondering if it's the best practice and if there is an easier way to check where I am in the app (which route).
My routes:
<Route component={ItemNew} path='/items/edit/:id' />
<Route component={ItemNew} path='/items/new' />
In my ItemNew component, I'd like to:
componentDidMount () {
let stateName = this.props.location.state
if(stateName === 'edit') {
this.props.fetchItem() // dispatch API action
}
}
The Route component used to perform the final part of the match on the current URL shows up on this.props.route; the entire list of nested Routes that participated in the match is in the this.props.routes array. You can pass any arbitrary props onto these Routes and retrieve them later. For example:
<Route component={ItemNew} name="edit" path='/items/edit/:id' />
<Route component={ItemNew} name="add" path='/items/new' />
and
componentDidMount () {
if(this.props.route.name === 'edit') {
this.props.fetchItem() //dispatch API action
}
}
Related
I have three different Users. I would want them to access different routes as per their role. I set a token and userRole in the localstorage and I would like to have my protected Route check the avaliability of token in the localstorage which is retrieved by redux.
const TeacherRoute =({ component: Component , auth, ...rest}) => (
<Route
{...rest}
render = {props =>{
if(!props.token){
return <Redirect to="/login" />
}else if(props.token !== null){
if (props.userRole !== 'principal'){
if (props.userRole === 'student'){
return <Redirect to="/studentdashboard" />
}else if(props.userRole ==='teacher'){
return <Redirect to="/teacherdashboard" />
}else{
return <Redirect to="/login" />
}
}
}else {
return < Component {...props} />;
}
}}
/>
);
const mapStateToProps = state => ({
token: state.auth.token,
userRole: state.auth.userRole,
});
export default connect(mapStateToProps, )(TeacherRoute);
The challenge is that at first when I had the StudentRoute before adding the teacher and AdminRoute the route could redirect and disallow those who are not students to have access to the Student Route but the moment I added the other two Routes I got this error.
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.```
May you kindly help me fix this problem. Thanks in advance
This has been solved. Instead of delegating the role checking and token checking to the react Router we relied upon the Layout wrappers for all the dashboards. Since they control what is displayed in their children they were the perfect place to store our authentication Logic.
I have a component in my react app which is a form. The form is used to create new licenses OR edit existing licenses. Either way it is only one component and it checks on componentDidMount() which "pageType" (add/update) it is.
Now to my problem, when I'm using the form to edit a license (licensee/:id/edit) and I’m clicking the button which is bidet to create a new license (licensee/add), it will not remount the component.
It will change the URL but all the preloaded data is still in the form.
LicenseeForm = Loadable({
loader: () => import('./license/LicenseeForm'),
loading: 'Loading..'
});
render() {
return (
<Router>
<Switch>
<LoginRoute exact path="/" component={this.LoginView}/>
<LoginRoute exact path="/login" component={this.LoginView}/>
<PrivateRoute exact path="/licensees/add" component={this.LicenseeForm}/>
<PrivateRoute exact path="/licensees/:id/update" component={this.LicenseeForm}/>
<Route path="*" component={this.NotFoundPage}/>
</Switch>
</Router>
)
}
const PrivateRoute = ({component: Component, ...rest}) => (
<Route
{...rest}
render={props =>
authService.checkIfAuthenticated() ? (<Component {...props} />) :
(<Redirect
to={{
pathname: "/login",
state: {from: props.location}
}}/>
)
}
/>
);
Component:
componentDidMount() {
const locationParts = this.props.location.pathname.split('/');
if (locationParts[locationParts.length-1] === 'add') {
this.setState({pageType: 'add'});
} else if (locationParts[locationParts.length-1] === 'update') {
this.setState({pageType: 'update'});
...
}}
EDIT
This is how it works now:
<PrivateRoute exact path="/licensees/add" key="add" component={this.LicenseeForm}/>
<PrivateRoute exact path="/licensees/:Id/update" key="update" component={this.LicenseeForm}/>
If you do need a component remount when route changes, you can pass a unique key to your component's key attribute (the key is associated with your path/route). So every time the route changes, the key will also change which triggers React component to unmount/remount.
When the route is same and only path variable changes which in your case is "id", then the component at the top level of your route receives the change in componentWillReceiveProps.
componentWillReceiveProps(nextProps) {
// In this case cdm is not called and only cwrp know
// that id has been changed so we have to updated our page as well
const newLicenseId = nextProps.match.params.id;
// Check id changed or not
if(currentLicenseId != newLicenseId) {
updateState(); // update state or reset state to initial state
}
}
I am pasting code which enables you to detect that page is changed and update the state or re-assign it to initial state. Also, suppose you come on license page first time then save current Id in a variable. That only you will use in componentWillReceiveProps to detect change.
Use props 'render' instead component.
As per Doc Component props remount while parent state changes but render props update.
https://reacttraining.com/react-router/web/api/Route/route-render-methods
I have a "home" component with links, and when you click a link the product component is loaded with the product. I also have another component which is always visible, showing links to the "recently visited products".
These links don't work when on a product page. The url updates when I click the link, and a render occurs, but the product component doesn't update with the new product.
See this example:
Codesandbox example
Here are the routes in index.js:
<BrowserRouter>
<div>
<Route
exact
path="/"
render={props => <Home products={this.state.products} />}
/>
<Route path="/products/:product" render={props => <Product {...props} />} />
<Route path="/" render={() => <ProductHistory />} />
<Link to="/">to Home</Link>
</div>
</BrowserRouter>;
The links in ProductHistory look like this:
<Link to={`/products/${product.product_id}`}> {product.name}</Link>
So they match the Route path="/products/:product".
When I am on a product page and try to follow a ProductHistory link, the URL updates and a render occurs, but the component data doesn't change. In the Codesandbox example you can uncomment the alert in Product components render function to see that it renders when you follow the link, but nothing happens.
I don't know what the problem is...Can you explain the problem and find a solution? That would be great!
Along with componentDidMount, You also need to implement the componentWillReceiveProps or use getDerivedStateFromProps(from v16.3.0 onwards) in Products page since the same component is re-rendered with updated params and not re-mounted when you change the route params, this is because params are passed as props to the component and on props change, React components re-render and not re-mounted.
EDIT: from v16.3.0 use getDerivedStateFromProps to set/update state based on props(no need to specify it in two different lifecyle methods)
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.match.params.product !== prevState.currentProductId){
const currentProductId = nextProps.match.params.product
const result = productlist.products.filter(obj => {
return obj.id === currentProductId;
})
return {
product: result[0],
currentId: currentProductId,
result
}
}
return null;
}
Prior v16.3.0, you would use componentWillReceiveProps
componentWillReceiveProps(nextProps) {
if (nextProps.match.params.product !== this.props.match.params.product) {
const currentProductId = nextProps.match.params.product
const result = productlist.products.filter(obj => {
return obj.id === currentProductId;
})
this.setState({
product: result[0],
currentId: currentProductId,
result
})
}
}
Working codesandbox
As Product component is already loaded it will not reload. You have to handle new product id in the below method of component
componentWillReceiveProps(nextProps) {
if(nextProps.match.params.name.product == oldProductId){
return;
}else {
//fetchnewProduct and set state to reload
}
With latest version of react(16.3.0 onwards)
static getDerivedStateFromProps(nextProps, prevState){
if(nextProps.productID !== prevState.productID){
return { productID: nextProps.productID};
}
else {
return null;
}
}
componentDidUpdate(prevProps, prevState) {
if(prevProps.productID !== this.state.productID){
//fetchnewProduct and set state to reload
}
}
Although all the above-mentioned ways will work, I don't see a point to use getDerivedStateFromProps.
Based on React docs, "if you want to re-compute some data only when a prop changes, use a memoization helper instead".
Here, instead, I would suggest simply using componentDidUpdate along with changing the Component to PureComponenet.
With reference to React docs, PureComponenets only rerender if at least one state or prop value changes. Change is determined by doing a shallow comparison of state and prop keys.
componentDidUpdate = (prevProps) => {
if(this.props.match.params.id !== prevProps.match.params.id ) {
// fetch the new product based and set it to the state of the component
};
};
Please note that the above only work if you change the Component to PureComponent, and obviously, you need to import it from React.
If you aren't maintaining state in your component, you can use componentDidUpdate without the need for getDerivedStateFromProps:
componentDidUpdate(prevProps) {
const { match: { params: { value } } } = this.props
if (prevProps.match.params.value !== value){
doSomething(this.props.match.params.value)
}
}
I'm using Redux with redux-simple-router.
Here's what I'm trying to do. A user hits a URL like so:
http://localhost:3000/#/profile/kSzHKGX
Where kSzHKGX is the ID of the profile. This should route to Profile container filled out with the details of the profile with id kSzHKGX.
My routes look like this:
export default (
<Route path="/" component={App}>
...
<Route path="profile" component={Profile} />
...
</Route>
)
So hitting the above link would give me Warning: [react-router] Location "undefined" did not match any routes
My container looks like this:
#connect(
state => state.profile,
dispatch => bindActionCreators(actionCreators, dispatch)
)
export class Profile extends Component {
constructor(props) {
super(props)
}
componentDidMount() {
const { getProfileIfNeeded, dispatch } = this.props
getProfileIfNeeded()
}
render() {
return (
<section>
...
</section>
)
}
}
So normally my container would just be populated from the state as usual in Redux.
Basically I need to have a way of doing some wildcard in the route. Than I need to pass the URL to the action that would pull up the right profile from an API. The question is, is it doable with react-simple-router? Can I do that somehow using UPDATE_PATH? Would it be the proper Redux way? Or should I use something else?
Following Josh David Miller's advice, I made my route look like so:
<Route path="admin/profile/:id" component={Profile} />
Than my container got this method to get the profile from API:
componentWillMount() {
const { getProfile, dispatch } = this.props
getProfile(this.props.params.id)
}
And this to cleanup (without it I would have the previous profile display for split second on component load - before I hit API in componentWillMount)
componentWillUnmount() {
this.props.unmountProfile()
}
Update:
As an alternative to the cleanup, I'm considering using the Container Component Pattern. Basically have the outer component do the data fetching and passing the data to the inner component as a prop.
I'm having trouble overcoming an issue with react router. The scenario is that i need to pass children routes a set of props from a state parent component and route.
what i would like to do is pass childRouteA its propsA, and pass childRouteB its propsB. However, the only way i can figure out how to do this is to pass RouteHandler both propsA and propsB which means every child route gets every child prop regardless of whether its relevant. this isnt a blocking issue at the moment, but i can see a time when i'd be using the two of the same component which means that keys on propA will overwritten by the keys by the keys of propB.
# routes
routes = (
<Route name='filter' handler={ Parent } >
<Route name='price' handler={ Child1 } />
<Route name='time' handler={ Child2 } />
</Route>
)
# Parent component
render: ->
<div>
<RouteHandler {...#allProps()} />
</div>
timeProps: ->
foo: 'bar'
priceProps: ->
baz: 'qux'
# assign = require 'object-assign'
allProps: ->
assign {}, timeProps(), priceProps()
This actually works the way i expect it to. When i link to /filters/time i get the Child2 component rendered. when i go to /filters/price i get the Child1 component rendered. the issue is that by doing this process, Child1 and Child2 are both passed allProps() even though they only need price and time props, respectively. This can become an issue if those two components have an identical prop name and in general is just not a good practice to bloat components with unneeded props (as there are more than 2 children in my actual case).
so in summary, is there a way to pass the RouteHandler timeProps when i go to the time route (filters/time) and only pass priceProps to RouteHandler when i go to the price route (filters/price) and avoid passing all props to all children routes?
I ran into a similar issue and discovered that you can access props set on the Route through this.props.route in your route component. Knowing this, I organized my components like this:
index.js
React.render((
<Router history={new HashHistory()}>
<Route component={App}>
<Route
path="/hello"
name="hello"
component={views.HelloView}
fruits={['orange', 'banana', 'grape']}
/>
</Route>
</Router>
), document.getElementById('app'));
App.js
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>{this.props.children}</div>;
}
}
HelloView.js
class HelloView extends React.Component {
constructor(props) {
super(props);
}
render() {
return <div>
<ul>
{this.props.route.fruits.map(fruit =>
<li key={fruit}>{fruit}</li>
)}
</ul>
</div>;
}
}
This is using react-router v1.0-beta3. Hope this helps!
Ok, now that I'm understanding your issue better, here's what you could try.
Since your child props are coming from a single parent, your parent component, not react-router, should be the one managing which child gets rendered so that you can control which props are passed.
You could try changing your route to use a param, then inspect that param in your parent component to render the appropriate child component.
Route
<Route name="filter" path="filter/:name" handler={Parent} />
Parent Component
render: function () {
if (this.props.params.name === 'price') {
return <Child1 {...this.getPriceProps()} />
} else if (this.props.params.name === 'time') {
return <Child2 {...this.getTimeProps()} />
} else {
// something else
}
}
In child component, insted of
return <div>{this.props.children}</div>
You may merge props with parent
var childrenWithProps = React.cloneElement(this.props.children, this.props);
return <div>{childrenWithProps}</div>
React.cloneElement can be used to render the child component and so as pass any data which is available inside the child route component which is defined in the route.
For eg, here I am passing the value of user to the react childRoute component.
{React.cloneElement(this.props.childRoute, { user: this.props.user })}