Using the same component for different Routes with React Router - javascript

I have a PageBuilder component that dynamically builds edit/list pages according to a configuration file. I want to have dynamic routes (like "/collection/list", "/collection/edit/123123", "/dashboard", etc.) that use the same PageBuilder component.
I'm having trouble getting this to work - if I'm in "/collection/list" for example, when clicking on a link to "/collection/edit/1231" doesn't work. Only a refresh to that URL works (and vice-versa).
I tried putting my initialization code PageBuilder's componentWilLReceiveProps but it seems to call it every second.
My routes look like this:
<Route path="/" component={App}>
<IndexRedirect to="/dashboard" />
<Route path="/:page/:collection/:action(/:entity_id)" component={PageBuilder} />
<Route path="/:page" component={PageBuilder} />
</Route>
And my PageBuilder:
constructor(props) {
super(props);
this.createSectionsHTML = this.createSectionsHTML.bind(this);
this.onChange = this.onChange.bind(this);
this.onSave = this.onSave.bind(this);
}
getPageName() {
return this.props.params.page.replace(/-/g, '_').toLowerCase();
}
componentWillReceiveProps(props) {
this.action = this.props.params.action;
}
componentWillMount() {
let pageName = this.getPageName();
this.props.dispatch(setInitialItem(pageName));
}
componentDidMount() {
let pageName = this.getPageName();
let { collection, entity_id } = this.props.params;
if (collection && entity_id) {
let { dispatch } = this.props;
dispatch(getCollectionEntity(collection, entity_id, pageName));
}
}
Any ideas of how to re-render the page each time I redirect to a different route?
It would be great if I could unmount and re-mount the component when redirecting, but I'm not sure how to go about telling React Router to do that....
Thanks!

Make this.state such that it will control how your component gets rendered.
Now, within componentWillReceiveProps, check the nextProps argument
componentWillReceiveProps(nextProps, nextState) {
if( <check in nextProps if my route has changed> ) {
let newState = Object.assign({}, this.state);
// make necessary changes to the nextState Object by calling
// functions which would change the rendering of the current page
this.setState({ nextState });
}
}
This would make componentWillReceiveProps take action only when the route changes.
Now in your render function,
render() {
const { necessary, variables, to, render } = this.state;
let renderVariables = this.utilityFunctionsReqToRender(someArgs);
return (
<toRenderJsx>
...
</toRenderJsx>
)
}
This would make your component "refresh" whenever the route changes.

componentWillReceiveProps is deprecated since React 16.3.0
(as says a warning in the browser console)
Reference :
https://reactjs.org/docs/react-component.html#componentdidupdate
So componentDidUpdate can be used to get the new state and reload data depending on params
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("componentDidUpdate " +prevState.id);
reloadData(prevState.id);
}

Related

React Re-renders When Parent State is Changed Through Child Component

In App.js, I am passing setURL(page){ ... } as a prop to HealthForm.
In HealthForm, I have an input field that takes a String of an URL and a button that initiates a fetch call to my backend server and some data is received back in a promise object. I also call that.props.changeUrl(that.state.someURL);inside the promiseStatus function because that's the only place I could place it without getting the following warning:
Warning: Can't call setState (or forceUpdate) on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
However, every time that that.props.changeUrl(that.state.someURL) is called, the page re-renders. Basically -- the input field and the additional functions that were rendered due to the fetch call -- all reset. The url state in App.js gets updated though.
Why does the whole page re-renders when I'm calling the parent props?
The app does not re-render if the line that.props.changeUrl(that.state.someURL) is simply deleted but of-course it doesn't change the App state
I need the page to not re-render because vital information is rendered after the fetch call which cannot be seen since the re-render resets that route.
App.js
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
url: '',
};
this.setURL = this.setURL.bind(this);
}
setURL(link) {
this.setState({
url: link
});
}
render(){
return(
<MuiThemeProvider>
<Router>
<div className="App">
<Route path="/" component={Header}></Route>
<Route path="/health" component={()=>(
<HealthForm changeUrl={this.setURL}/>)}></Route>
<Route path="/path1" component={wForm}></Route>
<Route path="/path2" component={xForm}></Route>
<Route path="/path3" component={yForm}></Route>
<Route path="/path4" component={zForm}></Route>
</div>
</Router>
</MuiThemeProvider>
);
}
}
HealthForm.js
class HealthForm extends React.Component {
constructor(props) {
super(props);
this.state = {
exampleURL: '',
exampleURLError: '',
status: '',
showStatus: false
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
validate = () => {
//…checks for input errors
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value
});
}
handleSubmit(event) {
event.preventDefault();
const err = this.validate();
let that = this;
if (!err) {
this.setState({
exampleURLError: ''
});
console.log(this.state);
var data = this.state.exampleURL
fetch('htpp://...', {
method: 'POST',
body: JSON.stringify(data)
})
.then((result) => {
var promiseStatus = result.text();
promiseStatus.then(function (value) {
that.setState({
status: value,
showStatus: true
});
that.props.changeUrl(that.state.jarvisURL);
});
}).catch((error) => {
console.log(error);
});
}
}
render() {
return (
<form>
<TextField
...
/>
<br/>
<Button variant="contained" size="small" color="primary" onClick={e => this.handleSubmit(e)} >
Check
</Button>
<br /> <br />
...
</form>
);
}
}
export default HealthForm;
This is happening because you're calling setState() on the App component, causing it to re-render, including re-creating all the routes you've set up. I'm not sure which router you're using exactly but it seems that it is recreating the components under the routes, probably by calling the component function that's passed in as a prop again and getting a new instance of your HealthForm component.
I assume the state you're storing inside App is required by all components in the application and that's why you're putting it there? If not, move it down into the HealthForm component, but if so maybe it's time to think about storing state externally to your components, e.g. in a state container like Redux or something else in a Flux style.
EDIT: I think the root of your problem is here:
<Route path="/health" component={()=>(<HealthForm changeUrl={this.setURL}/>)}></Route>
In the fact that a function is passed as the component prop, resulting in a new instance of the component each time. I can see why you needed to do that, to get the reference to setURL() passed into the HealthForm - it's also something that could be avoided by extracting the state out of the component.

Using lifecycle method with redux versus checking prop state in render method

Is there anything wrong with this flow in redux? I change isAuthenticated in my reducer like so
export function userReducer(state=initState, action) {
switch(action.type){
case AUTH_SUCCESS:
return {
...state,
isAuthenticated: true
}
//...
//...
}
}
so in my render I check isAuthenticated if equal to true I redirect the user to login.
#connect(state => state.user, {loginUser})
class LoginForm extends React.Component {
constructor() {
super()
this.state = {
email: '',
password: ''
}
}
handleSubmit() {
const { email, password } = this.state
this.props.login(email, password)
}
render(){
const { email, password } = this.state
return(
<div>
{this.props.user.isAuthenticated && <Redirect to={'/dashboard'} />}
<Form input={{email, password}} handleSubmit={this.handleSubmit} />
</div>
)
}
}
export default LoginForm
Another way is I can make the render method cleaner, I use componentWillRecieiveProps
componentWillReceiveProps(nextProps) {
if(nextProps.user.isAuthenticated !== this.props.user.isAuthenticated && nextProps.user.isAuthenticated) {
this.props.history.replace('/dashboard')
}
}
Which one is appropriate and why?
They are both valid ways, except componentWillReceiveProps is deprecated in react v16.3 so it would be better to use componentDidUpdate. And I would simplify logic as follows:
componentDidUpdate(prevProps) {
if(prevProps.user.isAuthenticated !== this.props.user.isAuthenticated) {
this.props.history.replace('/dashboard')
}
}
There is one difference though, that with Redirect you don't have to create class component but you can use it in stateless component and it doesn't even have to be inside Route or connected withRouter.
const SomeComponent = ({ isLogged }) => (
<div>
{isLogged <Redirect to="/path" />}
<div>something</div>
</div>
)
Both of your methods are equivalent since, Redirect also replaces the current history stack and so does this.props.history.replace(). You can use anyone of the methods.
Also considering that React wants to deprecate the componentWillReceiveProps from v17 onwards you are better of using componentDidUpdate for the same
However componentDidUpdate here has a slight disadvantage here that for a fraction of a second Form will be visible and then redirect will happen which might be a bad user experience.
So consider future development in React, you might use the Redirect approach instead of doing prop comparison and using history.replace()
As others have said, they're both valid.
Just from a react point of view, a login component should handle exactly that - logging in.
I would expect a higher component than the login page (the app perhaps), to do the logic of whether to display the Login form, or to redirect. E.g. something like this:
import {withRouter} from 'react-router-dom';
class App {
render() {
if (this.props.user.isAuthenticated && this.props.location === '/login') {
return <Redirect to={'/dashboard'} />;
}
return (<div>
REST OF APP HERE
</div>);
}
}
export default withRouter(props => <App {...props}/>);
The benefit here is you can easily test your login form, as well as compose it within other components really easily. Your routing concerns are effectively global, and those can live up at the top with your app.

React doesn't reload component data on route param change or query change

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)
}
}

Reducer states not received because of a Route in ReactJS

I have a very mysterious dilemma: I'm using Reducers to receive a User model from my backend. At first, the reducer returns a state of null (as expected because it's still pending the API call) and then the User model the second time around.
If that User model exists AND the customer tries to navigate to /setup, then we'll let him -- he's "authenticated". However, if he tries to access /setup and the User model doesn't exist, we redirect him back to the root path ("/").
I've made a function that will called within my App.js's Route element that checks this OAuth, called AuthenticatedRoute.js
When we get the initial value from the reducer (of null), we render the AuthenticatedRoute.js, see the value is null and then navigate to the root path. The problem is, when I get the SECOND value from the reducer (of the user model), the AuthenticatedRoute.js doesn't try to re-render at all. Isn't it suppose to? The state of the prop in App.js (the parent component) changed. It's "suppose" to check so we can check if the User model came back and if the customer is allowed to go to the /setup page.
App.js (parent component that contains the Route):
class App extends Component {
state = { isUserSetUp: false }
// If User exists AND its isProfileSetUp is true then change state to TRUE
componentWillReceiveProps(nextProps) {
if (nextProps.auth && nextProps.auth.isProfileSetUp) {
this.setState({ isUserSetUp: false }, function() {
console.log("This is when we receive the User model from the reducer");
});
}
else {
this.setState({ isUserSetUp: false });
}
}
// Upon mount of this component, we receive the User via a reducer endpoint
componentDidMount() {
this.props.fetchUser();
}
render() {
return (
<div className="container">
<BrowserRouter>
<div>
<Header />
<Route exact path="/" component={Landing} />
<Route exact path="/dashboard" component={Dashboard} />
{/* Profile Set up... This is using our AuthenticatedRoute*/}
<Route exact path="/setup" component={AuthenticatedRoute(Setup, this.state.isUserSetUp, Landing)} />
</div>
</BrowserRouter>
</div>
);
}
};
And here is the AuthenticatedRoute:
import React from 'react';
import { withRouter } from 'react-router';
import { Redirect } from "react-router-dom";
export default function requireAuth(Component, isAuthenticated, Landing) {
class AuthenticatedComponent extends React.Component {
componentWillMount() {
this.checkAuth();
}
// We call this only the first time...never the second time around
checkAuth() {
console.log("This is when we run the AuthenticatedRoute");
if (!isAuthenticated) {
// For some reason when this hits, I no longer get the second value (User model) from the reducer. WITHOUT it, I get the second value in this component.
this.props.history.push('/');
}
}
render() {
return isAuthenticated
? <Component { ...this.props } />
: null;
}
}
return withRouter(AuthenticatedComponent);
}
NOTE:
So without the "this.props.history.push('/') within the AuthenticatedRoute, I RECEIVE the second reducer response (I tried console.logging it from the AuthenticatedRoute's render() method)...But the moment I handle the reducer's first response with the this.props.history.push, I never get the second response.
Same deal if I use a within the:
render() {
return isAuthenticated
? <Component { ...this.props } />
: null;
}
Do you guys have any idea? This is too weird.

React Router - Rendering <Redirect> instead of a child list component

I have the following code:
import { Redirect } from 'react-router-dom';
import QPContent from '../QPContent';
class AnswerContainer extends Component {
constructor(props) {
super(props);
this.state = {
redirect: false
};
}
render() {
const { redirect } = this.state;
if (redirect) {
return <Redirect push to={'/question/'+this.props.qID}/>;
}
return (
<div className="qp-answer-container">
....
</div>
);
}
}
const mapDispatchToProps = (dispatch, ownprops) => ({
....
});
const mapStateToProps = (state) => ({
....
});
export default connect(mapStateToProps, mapDispatchToProps)(AnswerContainer);`
I have an array of AnswerContainer components in a parent component. I am currently in the route /question/qId. I am trying to refresh the page on setting redirect: true. So when I use setState() and change the state of only one component in the list to redirect: true, the parent component doesn't re-render. Instead, the child component disappears. Why is this happening and how to trigger re-render of the whole page on <Redirect/> ?
Note: I'm calling setState() inside an action dispatch
If you really want to re-render the whole page, just use window.location.reload().
The idea of <Redirect /> component is to update the current url, not to re-render components. Of course they usually do re-render since most commonly you display something else when the url changes. But the job of react is to re-render as little as possible.

Categories