Redirect with "componentDidMount()" : problem (React) - reactjs

I have some code that works. It immediately reroutes a user from the /test page to the FinishedPaying page. It is as so:
class Test extends Component {
renderRedirect = () => {
return <Redirect to="/FinishedPaying" />;
};
componentDidMount() {
this.renderRedirect();
}
...
The following code is meant to send a Paypal transaction, then route the user to the /FinishedPaying page. All of the other logic is working as expected:
export default class Pay extends React.Component {
state = {
userInput: ""
};
renderRedirect = () => {
return (
<Redirect
to="/FinishedPaying"
userInput={this.state.userInput}
/>
);
};
componentDidMount() {
this.setState({ userInput: this.props.userInput });
this.renderRedirect();
}
render() {
const onSuccess = payment => {
axios
.post(
"http://amazonaws.com:3000/ethhash",
{
userInput: this.props.userInput,
}
)
.then(response => console.log(response.data, payment))
.catch(function(error) {
console.log(error);
});
};
return (
<div>
<PaypalExpressBtn
onSuccess={onSuccess}
/>
</div>
);
}
}
Not sure why the second code block is working. It is my understanding that this.renderRedirect() should fire after all of the other logic has happened. It does not seem to be firing at all. Any feedback is appreciated :)

you can put it in your render like:
render() {
if (this.state.redirect){
return <Redirect
to="/FinishedPaying"
userInput={this.state.userInput}
/>;
}
const onSuccess = payment => {...}
As soon as you change your redirect value in state for true you will be redirected.

You can't return the component <Redirect to="/FinishedPaying" /> in componentDidMount, you can only do that in render().
You could have a flag that sets to true when you're ready to redirect:
componentDidMount() {
this.setState({
userInput: this.props.userInput,
readyToRedirect: true
});
}
Then in your render method:
render() {
this.state.readyToRedirect
? <Redirect to="/FinishedPaying" />
: other stuffs...
or in my opinion, a more readable way:
render() {
if (this.state.readyToRedirect) return <Redirect to="/FinishedPaying" />
return (
// rest of the code
)
I also wouldn't define onSuccess function inside render, every state change will trigger render and re-define the function again and again.
If it doesn't require anything from this, you can even put it outside of the class
const onSuccess = payment => {
...
}
export default class Pay extends React.Component {
...
}

export default class Pay extends React.Component {
state = {
redirect: false
};
renderRedirect = () => {
if(this.state.redirect){
return (
<Redirect
to="/FinishedPaying"
userInput={this.props.userInput}
/>
);
}
};
componentDidMount() {
this.setState({ redirect: true });
}
render() {
const onSuccess = payment => {
axios
.post(
"http://amazonaws.com:3000/ethhash",
{
userInput: this.props.userInput,
}
)
.then(response => console.log(response.data, payment))
.catch(function(error) {
console.log(error);
});
};
return (
<div>
{this.renderRedirect()}
<PaypalExpressBtn
onSuccess={onSuccess}
/>
</div>
);
}
}

Related

React HOC with Router not finding page

Hi I am working on a react app with Routing and HOC. I expect to see a page but i get page not found when i know the page is there.
in componentDidMount this.setState, data is shown as undefined but in the HOC wrapper i see the data arrive from the server.
Before I wrapped the page in HOC i could see it rendering content so I know the content exists.
Here is my Page component which is being called via a Route :
import React, { Component } from "react";
import WithBackend from "./WithBackend";
class Page extends Component {
constructor(props) {
super(props);
this.state = { model: null };
}
render() {
if (this.state.model != null) {
return (
<div className="container">
<div className="row">
<div className="col-md">
<h1>{this.state.model.title}</h1>
</div>
</div>
</div>
);
} else {
return (
<div>
<h2>Home</h2>
</div>
);
}
}
componentDidMount() {
const data = this.props.getPage("1");
console.log(data);
this.setState({
model: data,
});
}
}
export default WithBackend(Page);
Here is the HOC component WithBackend: I am not sure if i should be setting the state on this class on in the class that is being wrapped.
When i debug the code in the getPage method, in the setState part i see the data being populated from the backend server.
import React from "react";
import ContentService from "./ContentService";
const WithBackend = (WrappedComponent) => {
class HOC extends React.Component {
constructor() {
super();
this.contentService = new ContentService();
this.getPage = this.getPage.bind(this); // <-- Add this
}
getPage(id) {
this.contentService
.getPage(id)
.then((response) => response.json())
.then((data) => {
this.setState({ model: data });
})
.catch((e) => {
console.log(e);
});
}
render() {
return <WrappedComponent getPage={this.getPage} {...this.props} />;
}
}
return HOC;
};
export default WithBackend;
and here is the contentService which only returns a promise:
class ContentService {
pageUrl = process.env.REACT_APP_BASE_URL + "/pages/";
getPage(id) {
const path = this.pageUrl + id;
const fetchPromise = fetch(path, {
method: "GET",
});
return Promise.resolve(fetchPromise);
}
}
export default ContentService;
Could anyone please advice what i am doing wrong?
Thanks in advance.
getPage is an asynchronous method, that should return a promise:
getPage(id) {
return this.contentService
.getPage(id)
.then((response) => response.json())
.catch((e) => {
console.log(e);
});
}
And then
componentDidMount() {
this.props.getPage("1").then(model => this.setState({ model }));
}

Replacing componentWillMount() with constructor does not work

A class is used to intercept axios error. to bind arrow functions, componentDidMount() is being used. now I need to initiliaze data from the server so I have to use componentWillMount() except it will be removed in React 17 and warning message suggest I use constructor. When I do it gives me an error.
import React, {Component} from "react";
import Modal from "../../components/UI/Modal/Modal";
import Aux from "../Auxiliary/Auxiliary";
const withErrorHandler = (WrappedComponent, axios) => {
return class extends Component{
state = {
error: null
};
// componentWillMount() {
// axios.interceptors.request.use(request => {
// this.setState({
// error: null
// });
// return request;
// });
// axios.interceptors.response.use(res => res, (error) => {
// this.setState({
// error: error
// })
// });
// }
constructor(props) {
super(props);
axios.interceptors.request.use(request => {
this.setState({
error: null
});
return request;
});
axios.interceptors.response.use(res => res, (error) => {
this.setState({
error: error
})
});
}
errorConfirmedHandler = () => {
this.setState({error: null})
};
render() {
return (
<Aux>
<Modal show={this.state.error} modalClosed = {this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props}></WrappedComponent>
</Aux>
);
}
}
};
export default withErrorHandler;
I removed .json from URL to produce an error
class BurgerBuilder extends Component {
state = {
ingredients: null,
totalPrice: 4,
purchasable: false,
purchasing: false,
loading: false,
// axiosError: null
};
componentDidMount() {
axios.get('https://burger-m.firebaseio.com/ingredients').then(response => {
this.setState({ingredients: response.data});
}).catch(error => {});
}
..
export default withErrorHandler(BurgerBuilder, axios);
&
Error Message: "index.js:1 Warning: Can't call setState on a component that
is not yet mounted. This is a no-op, but it might indicate a bug in your
application. Instead, assign to `this.state` directly or define a `state = {};`
class property with the desired state in the _temp component."
componentWillMount() does work however. so What Should I change?
Keep constructor simple by just adding state and do not register axios interceptors in constructor method, instead register interceptors in render method.
componentWillUnmount(){
console.log('unregistering interceptors', this.reqInterceptor, this.resInterceptor)
axios.interceptors.request.eject(this.reqInterceptor);
axios.interceptors.response.eject(this.resInterceptor);
}
render() {
if(!this.resInterceptor){
console.log('Registering Interceptors');
this.reqInterceptor = axios.interceptors.request.use(req => {
this.setState({ error: null })
return req;
})
this.resInterceptor = axios.interceptors.response.use(response => response, error => {
this.setState({error})
})
}
return (
<Aux>
<Modal show={this.state.error} modalClosed={this.errorConfirmedHandler }>{this.state.error ? this.state.error.message : null}</Modal>
<WrappedComponent />
</Aux>
)
The constructor initializes the state, that's why you are prohibited from using setState() there.
You could use componentDidMount() instead, I think it matches better your needs and will avoid any confusion.
const withErrorHandler = (WrappedComponent, axios) => {
return class extends Component{
state = {
error: null
};
componentDidMount() {
axios.interceptors.request.use(request => {
this.setState({
error: null
});
return request;
});
axios.interceptors.response.use(res => res, (error) => {
this.setState({
error: error
})
});
}
errorConfirmedHandler = () => {
this.setState({error: null})
};
render() {
return (
<Aux>
<Modal show={this.state.error} modalClosed = {this.errorConfirmedHandler}>
{this.state.error ? this.state.error.message : null}
</Modal>
<WrappedComponent {...this.props}></WrappedComponent>
</Aux>
);
}
}
};
Use this.state = {error: null} and this.state = {error} instead of setState in then blocks of interceptors.
Regarding this specific example I think the best solution is to use a "temporary" variable , call the use method on the interceptors, and the define the state. All this inside the constructor.
Like so:
constructor() {
super();
let tempErrorState = null;
this.requestInterceptor = axios.interceptors.request.use(req => {
tempErrorState = null;
return req;
});
this.resoponseInterceptor = axios.interceptors.response.use(res => res, error => {
tempErrorState = error;
});
this.state = {
error: tempErrorState
};
}
As per your condition, you can't use the componentDidMount() method. You MUST NOT use the render() method for setting up Axios interceptors because you are using withErrorHandler as a higher-order component which can lead to many interceptors eventually created while wrapping other components too. This is because for each render cycle, you are unnecessarily setting up an interceptor if you define it inside a render() method. You should set up an Axios interceptor only once in the component.
The constructor() would be the best place to set this up (as per the latest version of React where componentWillMount() is deprecated).
You can edit your code to include a constructor():
constructor(props) {
super(props);
this.state = {
error: null
};
// clear the error when sending a request
axios.interceptors.request.use(req => {
this.state = {
error: null
};
return req;
});
axios.interceptors.response.use(res => res, err => {
this.state = {
error: err
};
});
}
Notice that here, you are not using the setState() method which throws a warning in constructor but you are directly setting the state, which is allowed only inside the constructor().

Cannot set the state of a unmounted component in react

I am new to the react and redux. Here is, what I am doing:
I have a component which is like ,
class LandingPage extends React.Component {
constructor(props) {
super(props);
this.state = {
isloading: true
}
}
componentDidMount() {
this.props.fetchJobDescription().then(() => {
this.setState({
isloading: false
})
});
}
render() {
if (this.state.isloading) {
return null;
}
else if (this.props.jobs && this.props.jobs.content && this.props.jobs.content.length > 0) {
return <JobList />;
}
else if (this.props.isError) {
return <ErrorComponent />
}
else {
return <Redirect to="/create-job" />
}
}
}
the action is like ,
export function fetchUserJd() {
return (dispatch) => {
let url = FETCH_JD_ROOT_URL + page + "&" + size;
dispatch({
type: REQUEST_INITIATED
})
return get(url)
.then((response) => {
if (response.status === 200) {
dispatch({
type: FETCHING_JOBDESCRIPTION_SUCCESS,
data: response.payload
})
dispatch({
type: REQUEST_SUCCESSED
})
} else {
if (!response.status) {
toastr.error('Our server is down. Please check again');
}
else if (response.status.status === 401) {
dispatch(logout());
}
else if (response.status.status === 500) {
toastr.error("Error while Fetching the job description,Please try again");
dispatch({
type: FETCHING_JOBDESCRIPTION_SUCCESS,
data: response.status,
});
dispatch({
type: REQUEST_SUCCESSED
})
} else {
dispatch({
type: REQUEST_SUCCESSED
})
}
}
})
return Promise.resolve();
}
};
Now,my logout is ,
export function logout() {
console.log("calling the logout action");
localStorage.clear();
history.push('/login');
return {
type: LOGOUT_REQUEST
}
}
class Header extends React.Component {
constructor(props) {
super(props);
}
logout = (e) => {
e.stopPropagation();
e.nativeEvent.stopImmediatePropagation();
e.preventDefault();
this.props.logout();
}
render() {
return (
<Fragment>
<Navigation
isAuthenticated={localStorage.getItem("access_token") ? true : false}
operationType={this.props.operationType}
logout={this.logout} />
</Fragment>
)
}
}
const mapStateToProps = (state) => {
return {
isAuthenticated: state.LoginReducer.isAuthenticated,
operationType: state.Header.operationType,
}
}
Here, when there is a invalid token like while fetching it gives me 401 unauthorized, then I redirect use for the logout action. now,
when I do this that time , I get an error:
Warning: Can't perform a React state update 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.
in LandingPage (created by Context.Consumer)
in Connect(LandingPage) (created by Route)
How I can resolve this error ?
The issue is because you are setting state after component has unmounted. The issue might be that you make api hit, component unmounts, Then response of api is returned which sets the state. If you are using axios it can be handled.
// in the component
signal = axios.CancelToken.source();
// in componentWillUnmount
this.signal.cancel('API was cancelled');
Its a small issue that happens in your code. When you receive a 401 token, you try to redirect to logout from within the action creator using history.push which will unmount your LandingPage component, but at the same time you are trying to setState with loading: false, thus you receive this warning. The solution is simple
class LandingPage extends React.Component {
constructor(props) {
super(props);
this.state = {
isloading: true
}
this._mounted = true;
}
componentDidMount() {
this.props.fetchJobDescription().then(() => {
if (this_mounted) {
this.setState({
isloading: false
})
}
});
}
componentWillUnmount() {
this._mounted = false;
}
render() {
if (this.state.isloading) {
return null;
}
else if (this.props.jobs && this.props.jobs.content && this.props.jobs.content.length > 0) {
return <JobList />;
}
else if (this.props.isError) {
return <ErrorComponent />
}
else {
return <Redirect to="/create-job" />
}
}
}
or else in the action creator you can throw an error instead of dispatching the logout action and in the .catch block of fetchJobDescription dispatch the logout action
In LandingPage
this.props.fetchJobDescription().then(() => {
this.setState({
isloading: false
})
}).catch((err) => {
this.props.logout();
});
and in action creator
else if (response.status.status === 401) {
throw new Error('Error in status')
}

Object property returning undefined after mounting component and setState

I'm using a callback from axios.get to set the state of a React component. The data property of the response contains an array of objects which I use to set state.
When I log the state property to the console there are no issues and I can see the array of objects. However if I try to log one of those objects individually I get the error:
Cannot read property '0' of null at ResponsiveTable.render
Below is the code for my component:
class ResponsiveTable extends React.Component {
constructor (props) {
super(props)
this.state = {
returnedQuery: null
};
}
componentDidMount() {
axios.get('/api/latestLeads')
.then((response) => {
this.setState({
returnedQuery: response.data
});
})
.catch(function (error) {
console.log(error);
});
}
render() {
console.log(this.state.returnedQuery[0]);
return (
<div>
<h1>test</h1>
</div>
);
}
}
Sure at first render your returnedQuery is null so you are getting this error.
If you want to use this.state.returnedQuery[0] check if it's exist and it's length > 0:
render() {
if (this.state.returnedQuery && this.state.returnedQuery.lenth > 0){
return (
<div>
{this.state.returnedQuery.map(...)}
</div>
);
} else {
return <div>loading data...</div>
}
}
You could try this:
class ResponsiveTable extends React.Component {
constructor (props) {
super(props)
this.state = {
returnedQuery: null
};
this.getData();
}
getData = () => {
axios.get('/api/latestLeads')
.then((response) => {
this.setState({
returnedQuery: response.data
}, () => {console.log(this.state.returnedQuery);}); //What does this console.log() say?
})
.catch(function (error) {
console.log(error);
});
}
render() {
console.log(this.state.returnedQuery[0]);
return (
<div>
<h1>test</h1>
</div>
);
}
}
I found the solution which was quite similar to Andrew's above, I used the component's internal state to determine whether the axios.get method had returned. Below is the working code, I'm now able to access elements within the returned array and their properties.
class ResponsiveTable extends React.Component {
constructor (props) {
super(props)
this.state = {
returnedQuery: null
};
}
componentDidMount() {
const self = this;
// let returnedQuery;
axios.get('/api/latestLeads')
.then((response) => {
self.setState({
returnedQuery: response.data
});
console.log(response.data);
})
.catch(function (error) {
console.log(error);
});
}
render() {
return (
<div>
<h1>{'This will always render'}</h1>
{ this.state && this.state.returnedQuery &&
<div>{this.state.returnedQuery[0].email}</div>
}
</div>
)
}
}

adding console.log to the class body causes error

when i add that console.log(3); to the class below it throws error
class AlbumList extends Component {
state ={ albums: [] };
console.log(3);
componentWillMount() {
console.log(4);
axios.get('https://rallycoding.herokuapp.com/api/music_albums')
.then(response => this.setState({ albums: response.data }));
}
renderAlbums() {
console.log(5);
return this.state.albums.map(
album => <AlbumDetail key={album.title} myAlbum={album} />
);
}
render() {
console.log(6);
return (
<View>
{this.renderAlbums()}
</View>
);
}
}
You need to put console.log(3) inside any method, example you can add it on componentDidMount()
componentDidMount() {
console.log(3)
}

Resources