React componentDidMount and working with Promises? - reactjs

Getting really fed up now! I am trying to get a Spinner element to appear while 3 functions run in the componentDidMount function.
From what I gather the render comes before componentDidMount, so I am running the Spinner in the render, while:
a cookie value is retrieved from this.getValidToken()
then an axios post request sets state of isLoggedin (using above value as payload)
then the logic() function runs a simple if statement to either log user in or redirect to
error page.
I keep getting errors about Promises, I feel there is a better way to do this?
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post =
axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
})
const LoggedIn = this.state.isLoggedIn;
const logic = () => {
if (LoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}

Firstly, you assign axios post to a variable, it is executed immediately and not after the getValidToken promise is resoved
Secondly the state update in react is async so you cannot have loggedIn logic based on state in promise resolver
You could handle the above scenario something like
constructor(props){
super(props);
this.state = {
isLoggedIn: false
}
}
componentDidMount() {
const post = () => axios.post(//api post request here)
.then(function(response) {
this.setState({ isLoggedIn: true });
return true;
})
.catch(function(error) {
this.setState({ isLoggedIn: false });
return false;
})
const logic = (isLoggedIn) => { // use promise chaining here
if (isLoggedIn) {
//log user in
} else {
//redirect user to another page
}
};
this.getValidToken()
.then(post)
.then(logic);
//getValidToken firstly gets a cookie value which is then a payload for the post function
}
render() {
return <Spinner />;
}

Related

Trouble passing props to imported component

I have a "main" component which imports two other components, which contain a form each, and I'm having trouble passing some values which I get from an API call in the main component.
This is an example of what's happening on the "main" component with one of the imported forms:
user_id = null;
email = "";
componentDidMount() {
get(`user-data/`, this.token).then((response) => {
this.user_id = response.user_id;
this.email = response.email;
// Doing a console.log at this point shows both values are assigned properly
// And that they exist
}).catch((error) => notify.notifyError(error.message));
}
<UserForm
email={this.email}
id={this.user_id}
></UserForm>;
Edit: I'm including the API call logic since I believe it has something to do with the issue.
export const get = async (url, authToken) => {
try {
const response = await axiosInstance.get(`${url}`, {
headers: {
"Content-Type": "application/json",
Authorization: `${BEARER} ${authToken}`,
},
});
return response;
} catch (error) {
throw new Error();
}
};
On the UserForm component:
id = null;
email = "";
constructor(props) {
super(props);
this.id = props.id;
this.email = props.email;
// Doing a console.log here shows both props are empty
// Trying to use either of them from here on out breaks the page
}
I assume the issue has to do with the components rendering before the value gets assigned, but I'm not entirely sure about it.
Why aren't the props received properly on the imported form, and how can I make sure they are?
Edit 2: Waiting for props to be set on componentDidUpdate works, but operating the way I need to creates and endless loop of execution of componentDidUpdate
componentDidUpdate() {
if (this.props.id) {
console.log("props are set!");
this.getData();
}
}
getData() {
get(`user-data/${this.props.id}`, this.token)
.then((user) => {
// This creates and endless loop of updating
this.setState({
...user.data
});
})
.catch((error) => notify.notifyError(error.message));
}
in your main component do like this
state = {
email: '',
user_id: null
}
componentDidMount() {
get(`user-data/`, this.token).then((response) => {
this.setState({
email: response.email,
user_id: response.user_id
})
// Doing a console.log at this point shows both values are assigned properly
// And that they exist
}).catch((error) => notify.notifyError(error.message));
}
and in your userform print your props like
componentDidUpdate(prevProps){
console.log('this.props', this.props);
console.log('prevProps', prevProps)
}
Since this two values are not managed by state, when the values varies, it won't triggered re-render. Hence, the props are not getting the up-to-date value. I suggest using state to handle those 2 values
constructor(props){
this.state = {
user_id = null;
email = "";
}
}
componentDidMount() {
get(`user-data/`, this.token).then((response) => {
console.log('response works fine?', response)
this.setState = {
user_id = response.user_id;
email = response.email;
}
// Doing a console.log at this point shows both values are assigned properly
// And that they exist
}).catch((error) => notify.notifyError(error.message));
}
render(){
<UserForm
email={this.state.email}
id={this.state.user_id}
></UserForm>;
}
In your UseForm component, there is no need to place it in state unless you want to do further modification. I suggest directly test them in render method.
constructor(props) {
super(props);
}
render(){
const {id, email} = this.props
return (
<div>
{id} and {email}
</div>
)
}

Where should I dispatch actions to call services to retrieve data needed before initial app load?

I need to call some services to retrieve data before showing any UI. Some of the data returned will be used for conditional rendering of error scenarios etc. on app load. Would it be best to do this just before ReactDOM.render()?
You can do something like this...
class Custom extends Component {
constructor(props) {
super(props);
this.state = { data: null, loading: false };
}
getData = () => {
// This is just to simulate a delayed API fetch/response.
this.setState({ loading: true }, () => {
setTimeout(() => this.setState({loading: false, data: {a: 'data loaded'}), 1000);
});
}
componentDidMount() {
this.getData(); // Call your data fetching method here...
}
render() {
const { loading, data } = this.state;
// you can also return a loading spinner instead of null;
// returning null ensures nothing is rendered in the DOM.
if (loading) return null;
// At this point, data is already available...
return (
<div>
// rest of your markup.
</div>
);
}
}
ReactDOM.render(<Custom />, document.getElementById('root'));
You should call them in componentDidMount
You can display a loader until the content is finished loading.

ReactJS memory leak while async tasks & subscriptions

In my react app I am making API call using axios. But, in my console, I got 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."
My code is below
To fix the memory leak, I added _isMounted check and consoled the _ismounted. My app is rendered twice.
First-time console prints _isMounted status true and then false (due to componentDidMount) but then the app is rendered second time & _isMounted in the console is printed as true.
app.jsx
export class Test extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
apiData: null
}
}
componentDidMount() {
this._isMounted = true;
API.callAPI(this.setStateHandler, this._isMounted)
}
setStateHandler = (state) => {
this.setState(state);
}
componentWillUnmount() {
this._isMounted = false
}
render() {
return(...)}
api.js
callAPI = (setStateHandler, _isMounted) => {
axios.get('/article', {headers: {Authorization: token}})
.then((response) => {
if(_isMounted) {
setStateHandler({ programs: response.data.data.programs });
}
})
}
I would like to fix my memory leak issue. How should I fix that?
Why is my application rendered twice and the second time componentDidUnmount is not called?
I would appreciate the help.
The _isMounted argument passed in to callAPI will not change when your component is unmounted.
You could instead return the data to the component and check if this._isMounted is still true there instead.
Example
// App.jsx
export class Test extends Component {
_isMounted = false;
state = { programs: null };
componentDidMount() {
this._isMounted = true;
API.callAPI().then(data => {
if (this._isMounted) {
this.setState({ programs: data.data.programs });
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
// ...
}
}
// api.js
const callAPI = () => {
return axios
.get("/article", { headers: { Authorization: token } })
.then(response => response.data);
};

ReactJS componentDidMount does not produce the value before rendering

I have the following code and getting the values through the api and set to the state variable but the view is rendered before setting the value to the state. So i could not display the value in my view. How could i change the code to work fine?
this.state = {
activeJobs: [],
isLoading: true
};
componentDidMount(){
axios.get(this.state.url+'/tables')
.then(response => {
// If request is good...
const isLoading = true,
activeJobs = response.data.activeJobs;
this.setState({ activeJobs });
})
.catch((error) => {
console.log('error ' + error);
});
}
render() {
console.log(this.state.activeJobs)
<p className="text">{!this.state.isLoading && this.state.activeJobs.count} Jobs</p>
}
The console i have given inside the render shows blank array. I also tried by changing the function componentDidMount() to componentWillMount() but getting the same result.
There is no way to ensure that an async request will complete before rendering. You can display proper messages in render to reflect the status of the request.
For example - before calling axios, set the state to 'in process' or 'loading', so that render will show an appropriate message. Then, when loading finished successfully or with an error, set the state appropriately to render an appropriate message in the error case, and the result otherwise.
If you can't render yet, then simply return null:
render() {
if (!this.state.activeJobs && !this.state.isLoading) {
return null;
}
return (
<div>
{ this.state.isLoading && <p className="text">Loading...</p> }
{ !this.state.isLoading && <p className="test">{ this.state.activeJobs.count } Jobs</p>
</div>
);
}
In order to set isLoading, set it before the HTTP call:
componentDidMount(){
this.setState({ isLoading: true });
axios.get(this.state.url+'/tables')
.then(response => {
// If request is good...
const activeJobs = response.data.activeJobs;
this.setState({ activeJobs, isLoading: false });
})
.catch((error) => {
console.log('error ' + error);
});
}

ReactJs: How to wait for componentDidMount() to finish before rendering?

How to wait for async componentDidMount() to finish before rendering?
My app.jsx:
constructor(props) {
super(props);
this.state = {
loggedInUser: null,
isAuthenticated: false,
isAuthenticating: true
};
}
componentDidMount() {
try {
var user = authUser();
console.log('User: ' + user)
if (user) {
console.log('Is logged in: ' + this.state.loggedInUser)
this.userHasAuthenticated(true);
}
}
catch(e) {
alert(e);
}
this.setState({ isAuthenticating: false });
}
render() {
console.log('in render: ' + this.state.loggedInUser)
// Should execute **after** authUser() in componentDidMount has finished
...
}
componentDidMount calls this async function:
function authUser() {
firebase.auth().onAuthStateChanged(function(user) {
return user
})
}
console.log('in render: ' + this.state.loggedInUser)
How can I make the render method wait for authUser() in componentDidMount?
Don't wait for componentDidMount to finish before rendering, that would be a misuse of the library, wait for your authUser to finish.
You can do that by utilising your isAuthenticating state property in combination with promises.
function authUser() {
return new Promise(function (resolve, reject) {
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
resolve(user);
} else {
reject('User not logged in');
}
});
});
}
You could use your existing isAuthenticating flag as follows:
componentDidMount() {
authUser().then((user) => {
this.userHasAuthenticated(true);
this.setState({ isAuthenticating: false });
}, (error) => {
this.setState({ isAuthenticating: false });
alert(e);
});
}
Then inside render:
render() {
if (this.state.isAuthenticating) return null;
...
}
This will prevent your component from being added to the DOM until your authUser function completes.
Your authUser() function doesn't seem to be set up correctly. You're returning the user object in the callback, but the function itself is not returning anything so var user = authUser(); will always return undefined.
You'll need to change authUser() to either call a callback function or return a Promise that resolves when the user is returned from Firebase. Then set the authentication status to your state once the promise is resolved or the callback is executed. In your render() function return null if the authentication has not yet finished.
Async function with callback:
function authUser(callback) {
firebase.auth().onAuthStateChanged(function(user) {
callback(user);
})
}
Using the callback with your component:
componentDidMount() {
try {
authUser(function(user) {
console.log('User: ' + user)
if (user) {
console.log('Is logged in: ' + this.state.loggedInUser)
this.userHasAuthenticated(true);
this.setState({ isAuthenticating: false });
}
});
}
catch(e) {
alert(e);
}
}
render() {
console.log('in render: ' + this.state.loggedInUser)
if (this.state.isAuthenticating === true) {
return null;
}
// Rest of component rendering here
}
componentDidMount will always fire after the first render.
either use componentWillMount or live with the second render, setState triggers a new render and componentWillMount always fires after the component did mount, i.e it rendered correctly.
If you want the component to not being rendered, wrap your component with some custom authorization component and don't render your component if the user is not logged in.
It's bad practice to try preventing the render function to call.
I think here the problem is that authUser is async. I would use promises in order to handle in a clean way the async response. I do not know firebase API but apparently support promises: https://firebase.google.com/docs/functions/terminate-functions
Otherwise you could use a library like bluebird and modify your authUser function to return a promise: http://bluebirdjs.com/docs/working-with-callbacks.html
If your are not familiar with promises, you should first of all read about the fundamentals: https://bitsofco.de/javascript-promises-101/

Resources