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/
Related
I am new guy in react, so how to put value that i receive from jsonHandler function into render return statement?
I've tried a lot, but i always have the same result. Console.log(jsonData) in jsonHandler function returns value that i need, but function jsonHandler returns promise and idk how to handle it. It doesn't matter to use axios.get or fetch().
async function jsonHandler () {
let jsonData;
const url = "http://localhost/index.php";
await axios.get(url)
.then(({data}) => {
jsonData = data.data;
});
console.log(jsonData); //returns that i need
return jsonData;
}
class Menu extends Component {
...
render() {
console.log(jsonHandler()); //returns promise
return <div className="MenuWrap">{this.mainHandler(Here Must Be Data From jsonHandler)}</div>;
}
}
export default Menu;
You can do it this way. Make use of states for reactive updates. I referred to https://stackoverflow.com/a/45744345/13965360 for the setStateAsync function, which sets the state value asynchronously and means it will wait till the API call is done. You can use try and catch with await if you are using async instead of then and catch blocks.
const url = "http://localhost/index.php";
class Menu extends Component {
state = {
jsonData: {},
};
//creating a function that sets the state asynchronously
setStateAsync(state) {
return new Promise((resolve) => {
this.setState(state, resolve);
});
}
// Fetch the data
async jsonHandler() {
try {
const response = await axios.get(url);
this.setStateAsync({ jsonData: response.data });
console.log(this.state.jsonData); //returns that i need
} catch (error) {
throw new Error(error);
}
}
render() {
return (
<div className="MenuWrap">
{Object.keys(this.state.jsonData).length &&
JSON.stringify(this.state.jsonData)}
</div>
);
}
}
export default Menu;
If you want to do the API call instantly once the component renders, you need to put it in the componentDidMount lifecycle.
Like, async componentDidMount() { await this.jsonHandler(); }
Or, if you want to make the API call upon clicking a button, you need to bind the method to the listener like <button onClick={this.jsonHandler}>.
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 />;
}
I need to test that when button is clicked and after promise resolve
state.message === 'loggedIn successfully'
class Login extends Component {
constructor() {
this.onLoginClick = this.onLoginClick.bind(this);
}
fetchLogin() {
return new Promise(function (resolve) {
reollve({ success: true });
})
}
onLoginClick() {
let that = this;
fetchLogin().then(function ({ success }) {
success ?
that.setState({ message: 'loggedIn successfully' }) :
that.setState({ message: 'Fail' });
})
}
render() {
return (<div>
<button onClick={this.onLoginClick}></button>
</div>)
}
}
I guess you are aware of jest simulate in order to simulate your click ( if not then simulate)
You should be able to use jest async/await or promises with jest, here is the link to the official doc
It should be something like this :
it('works with async/await and resolves', async () => {
const wrapper = mount(<Login />);
wrapper.find('button').simulate('click');
await expect(wrapper.state().message).resolves.toEqual('loggedIn successfully');
});
Since you are performing a test on a promise, you should wait for the function triggered by the button to execute before you can make the assertion (thus the await).
The code below should work for you case:
it('should change state on successful login', async () => {
const wrapper = mount(<Login />);
await wrapper.find('button').simulate('click');
expect(wrapper.state().message).toEqual("loggedIn successfully");
});
I am running this code:
.then((url) => {
if (url == null || undefined) {
return this.props.image;
} else {
const { image } = this.props;
//entryUpdate is an action creator in redux.
this.props.entryUpdate({ prop: 'image', value: url })
.then(() => {
this.setState({ loading: false });
});
but I get the following error:
How do I format setState() inside an asynchronous function that's called after an action creator?
Any help would be much appreciated!
In order for this to work, your action creator this.props.entryUpdate would need to return a promise for the async work it's doing. Looking at the error message, that does currently not appear to be the case.
You also need to be aware that calling setState() in the asynchronous callback can lead to errors when the component has already unmounted when the promise resolves.
Generally a better way is probably to use componentWillReceiveProps to wait for the new value to flow into the component and trigger setState then.
I placed the .then() function inside of the the if statement. But it should be like this:
.then((url) => {
if (url == null || undefined) {
return this.props.image;
} else {
const { image } = this.props;
this.props.entryUpdate({ prop: 'image', value: url })
}
})
.then(() => {
this.setState({ loading: false });
});
Need to load my main component and, in case a localstorage with the pair value "logged: true" exists redirect to "/app" using react-router.
I am using react-redux and this is my code:
class Main extends Component {
componentWillMount(){
// Return true in redux state if localstorage is found
this.props.checkLogStatus();
}
componentDidMount(){
// redirect in case redux state returns logged = true
if(this.props.logStatus.logged){
hashHistory.push('/app');
}
}
render() {
return (
<App centered={true} className="_main">
{this.props.children}
</App>
);
}
}
My redux action:
checkLogStatus() {
// check if user is logged and set it to state
return {
type: LOGIN_STATUS,
payload: window.localStorage.sugarlockLogged === "true"
};
}
But when the component gets to the componentDidMount stage, my redux state has still not been updated.
Y manage to get this to work by using:
componentWillReceiveProps(nextProps){
if (nextProps.logStatus.logged && nextProps.logStatus.logged !== this.props.logStatus.logged){
hashHistory.push('/app');
}
}
But I am not sure it is the most elegant solution.
Thanks in advance!
Using componentWillReceiveProps is the way to go here since your logStatus object is being passed in as a prop that is being changed.
There is a more elegant way to this using the Redux-thunk middleware which allows you to dispatch a function (which receives dispatch as an argument instead of the object actions. You can then wrap that function in a promise and use it in componentWillMount.
In your actions file:
updateReduxStore(data) {
return {
type: LOGIN_STATUS,
payload: data.logInCheck
};
}
validateLocalStorage() {
...
}
checkLogStatus() {
return function(dispatch) {
return new Promise((resolve, reject) => {
validateLocalStorage().then((data) => {
if (JSON.parse(data).length > 0) {
dispatch(updateReduxStore(data));
resolve('valid login');
} else {
reject('invalid login');
}
});
});
};
}
Then in your component:
componentWillMount() {
this.props.checkLogStatus()
.then((message) => {
console.log(message); //valid login
hashHistory.push('/app');
})
.catch((err) => {
console.log(err); //invalid login
});
}
Redux-thunk middleware is made for such use cases.