React setState() not updating state after $.ajax() request - reactjs

I'm using react with react-router. After checking authentication with onEnter Asynchronous hook on IndexRoute, App component gets rendered. App component has an initial state auth which is set to undefined when it renders. auth state is being passed to Navbar component as prop where it will be used to decide whether or not to show login, register and logout links.
When App component is done rendering, componentDidMount() makes an ajax call to again check if user is authenticated. On response it makes change to the state. After state change from ajax request, i'm logging state to console, this.setState() method is not changing state but somehow still triggers componentWillReceiveProps() method on Navbar component and this.props.auth value is still undefined.
// Checks Authentication Asynchronously
isAuthenticated(nextState, replace, callback) {
$.ajax({
type : 'GET',
url : '/auth',
success : function(res){
if(!res){
callback(replace({ pathname: '/login', query: { auth: 'false' } }));
}else{
callback();
}
}
});
};
// routes
var routes = (
<Router history={browserHistory}>
<Route path="/" component={require('./components/app')}>
<IndexRoute component={require('./components/dashboard/index')} onEnter={Auth.isAuthenticated}/>
<Route path="/register"
component={require('./components/authentication/register')}
onEnter={Auth.isNotAuthenticated} />
<Route path="/login"
component={require('./components/authentication/login')}
onEnter={Auth.isNotAuthenticated}/>
<Route path="*"
component={require('./components/404/404')}/>
</Route>
</Router>
);
// App
const App = React.createClass({
getInitialState(){
return {
auth : undefined
}
},
componentDidMount(){
console.log('App componentDidMount');
this.checkAuth();
},
checkAuth(){
var self = this;
$.ajax({
type : 'GET',
url : '/auth',
success : function(res){
if(res){
self.setState({
auth : true
});
}else{
self.setState({ auth : false});
}
}
});
console.log(this.state.auth);
},
render() {
return(
<div className="appWrapper">
<Navbar auth={this.state.auth}/>
<div className="container">
{this.props.children}
</div>
</div>
);
}
});
// Navbar
var Navbar = React.createClass({
getInitialState(){
return{
user_actions : '' ,
auth : this.props.auth
}
},
componentDidMount(){
console.log('Navbar componentDidMount ', this.props.auth);
this.checkAuthState();
},
componentWillReceiveProps(){
console.log('Navbar componentWillReceiveProps ', this.props.auth);
this.setState({
auth : this.props.auth
});
this.checkAuthState();
},
checkAuthState(){
console.log('Nav Mounted with auth : ', this.state.auth);
if(this.state.auth == undefined){
this.state.user_actions = '';
}
if(!this.state.auth){
this.state.user_actions = <ul className="nav navbar-nav navbar-right">
<li>Login</li>
<li>Register</li>
</ul>;
this.setState({
user_actions : this.state.user_actions
});
}
if(this.state.auth){
this.state.user_actions = <ul className="nav navbar-nav navbar-right">
<li>Logout</li>
</ul>;
this.setState({
user_actions : this.state.user_actions
});
}
},
render : function(){
return (
<nav className="navbar navbar-default">
<div className="container">
Reactor
{this.state.user_actions}
</div>
</nav>
);
}
});

First of all, I suggest you to reread React.JS documentation, because there are couple of things that need to be noted:
Never mutate this.state directly, use setState method instead. (line: 108, 111, 121, 133, 136, 146)
You should use state for storing data that changes over time, not an element. (line: 111, 121, 136, 146)
tl;dr;
Let's go back to the questions:
1. Ajax response is changing a state value, but the value isn't change in your log.
You won't see it if you print the value after ajax request! The reason is:
First, you're doing asynchronous request using Ajax and trying to see the result in synchronous way. JS will execute your console.log first which still contains the value before request, and then perform ajax request callback. This is the block of your code:
$.ajax({ ...,
success: function(res) {
if(res) { self.setState({ auth : true }); }/
...
} // will executed later (after ajax get response)
});
console.log(this.state.auth); // will executed first, this is why it always prints the value as undefined
Second, you won't able to see the changed state value right after you set a new state value. For instance, let say the value of this.state.auth is false:
this.setState({ auth: true});
console.log(this.state.auth); // will print false, instead of true as your new value
You're able to see your new state value by using componentWillUpdate(nextProps, nextState) method. You can read about this from this link: React.JS Component Specs and Lifecycle
2. Still triggers componentWillReceiveProps() method on Navbar component and this.props.auth value is still undefined.
It means that your state value is successfully changed by setState() on your ajax response. The proof is Navbar component receive a new props that send it down by App component (where the auth state is changed) which will trigger componentWillReceiveProps() method.
Maybe your code should be like this:
// App
const App = React.createClass({
getInitialState : function(){
return {
auth : false
}
},
componentDidMount : function() {
console.log('App componentDidMount');
this.checkAuth();
},
componentWillUpdate : function(nextProps, nextState) {
//you'll see the changing state value in here
console.log('Your prev auth state: ' + this.state.auth);
console.log('Your next auth state: ' + nextState.auth);
},
checkAuth : function(){
var self = this;
$.ajax({
type : 'GET',
url : '/auth',
success : function(res){
if(res){
self.setState({ auth : true });
}
}
});
},
render : function(){
return(
<div className="appWrapper">
<Navbar auth={this.state.auth}/>
<div className="container">
{this.props.children}
</div>
</div>
);
}
});
// Navbar
// Because the navbar component receive data (this.props.auth) from parent (app) via props, so we're no longer need to assign auth as a state in Navbar component.
const Navbar = React.createClass({
render : function(){
// you're no longer need checkAuthState method
let navItems;
if(!this.props.auth){
navItems = (<ul className="nav navbar-nav navbar-right">
<li>Login</li>
<li>Register</li>
</ul>);
} else {
navItems = (<ul className="nav navbar-nav navbar-right">
<li>Logout</li>
</ul>);
}
return (
<nav className="navbar navbar-default">
<div className="container">
Reactor
{ navItems }
</div>
</nav>
);
}
});
Hope it helps!

In ajax scope. it is unable to access react state. as a alternative you can call other method in the module as ajax success call, then update state there.
follow this example.
var reactModule = React.createClass({
getInitialState:function(){
},
render: function() {
return (
<div>
content
</div>
);
},
componentDidMount: function() {
var ajaxSuccess=this.ajaxSuccess;
$.ajax({
type: "POST",
url: $api_url + 'index.php/app/icon1_update',
dataType: "text",
data:fd,
contentType: false,
processData: false,
success: ajaxSuccess
});
},
ajaxSuccess:function(e){
//e is the result. update state here.
}
});

Just use arrow functions to access ‘this’:
success: () => {
this.setState({ data: value })
}

please check the Documentation of componentWillReceiveProps:
componentWillReceiveProps(
object nextProps
)
https://facebook.github.io/react/docs/component-specs.html#updating-componentwillreceiveprops
When your properties will change, then access to the properties "nextProps". Otherwise you will access to the old properties.
As a small hint:
Include your checkAuthState() code in the render method, not in the componentDidMount method because therefore you can avoid your setState call.

Related

Warning: Can't call setState (or forceUpdate) on an unmounted component issue on my project

I am using fetching google geometry data by using redux actions. I have an issue about this. When redirect to a new page, I have a warning message : 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.
Actually, I am pretty new about React. Can anyone help me to resolve this issue?
The followings are codes I have built as of now.
class BranchList extends Component {
startInterval;
state = {
city: "",
_isMounted: false
};
setTodayWeatherInfo = city => {
// invoke actions with parameter, city
// once I remove this one, no warning message generated
this.props.setLocation(city);
this.props.additionalTodayWeatherInfo(city);
if (this.startInterval) clearInterval(this.startInterval);
this.startInterval = setInterval(() => {
this.props.additionalTodayWeatherInfo(city);
}, 300000);
};
componentDidMount() {
const city = sessionStorage.branch_city || options[0].value;
this.setState({
city,
_isMounted: true
});
this.setTodayWeatherInfo(city);
}
shouldComponentUpdate(nextProps, nextState) {
return this.state.city !== nextState.city ? true : false;
}
componentWillUnmount() {
this.setState({ _isMounted: false });
clearInterval(this.startInterval);
this.startInterval = false;
}
render() {
if (!this.state.city) return <div />;
//this.props.location(this.state.city);
return (
<div>
<nav className="navbar navbar-expand-sm bg-warning">
<div className="text-center w-100">
<h4>Welcome to Korean Restaurant in {`${this.state.city}`}</h4>
</div>
<div className="mx-auto text-center w-50">
<SelectCity
setCity={city => {
this.setState({ city });
this.setTodayWeatherInfo(city);
}}
refreshStatus={this.props.refreshStatus}
/>
</div>
</nav>
</div>
);
}
}
export default connect(
null,
{ additionalTodayWeatherInfo, setLocation }
)(BranchList);
Then, this.props.setLocation(city) call actions as followed. Once I get rid of this.props.setLocation(city), it does not generate the warning message. I need to give up some functions associated with this, though.
export function setLocation(branch_city) {
const url = `${GoogleURL}=${branch_city}&key=${
process.env.REACT_APP_GMAP_API_KEY
}`;
const request = axios.get(url);
return {
type: FETCH_LOCATION,
payload: request
};
}
Is there any way to prevent the warning message?
I dont know what exactly this.props.setLocation(city) does, but I believe it is definitely affecting the DOM, the issue here is, even after you get out of this BranchList component, you are still trying to change the component which is not present in the DOM anymore, just as the way you cleared the setInterval in componentWillUnmount method, try to stop calling setLocation method

Child component's state does not change while rendering

I am a novice to ReactJs so please bear with me. I am trying to build a project from this course called Git-Hub profile viewer. Here is my code for Parent component:
import Profile from './github/Profile.jsx';
class App extends Component {
constructor(props){
super(props);
this.state = {
username: 'xxxx',
userData: [],
userRepos: [],
perPage: 5
}
}
// get user data from github
getUserData(){
$.ajax({
url: 'https://api.github.com/users/' +this.state.username+'?client_id='+this.props.clientId+'&client_secret='+this.props.clientSecret,
dataType:'json',
cache:false,
success: function(data){
this.setState({userData:data});
console.log(data);
}.bind(this),
error: function(xhr, status, err){
this.setState({userData: null});
alert(err);
}.bind(this)
});
}
componentDidMount(){
this.getUserData();
}
render(){
return (
<div>
<Profile userData = {this.state.userData} />
</div>
)
}
}
App.propTypes = {
clientId: React.PropTypes.string,
clientSecret: React.PropTypes.string
};
App.defaultProps = {
clientId: 'some_genuine_client_Id',
clientSecret:'some_really_client_secret'
}
export default App;
and here is my child's component:
class Profile extends Component {
render() {
return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">{this.props.userData.name}</h3>
</div>
<div className="panel-body">
</div>
</div>
)
}
}
export default Profile;
Problem is that the props in child component does not update its state while rendering the page although the data retrieval is successful from Git-Hub as shown in console log. What am I doing wrong, can somebody please help?
So firstly, you should be ensuring that you are accessing userData in the correct way, as #mersocarlin mentioned. This is quite likely to be the cause of your problem.
Theoretically, your way of doing this should work. However I've written a working jsfiddle for you as an example. I couldn't make the Ajax call so have simulated it with a setTimeout event over 5 seconds.
The other way of doing this is to pass down the getUserData function to the child component (Profile) and let it handle the call itself. Here is the jsfiddle for that, and the code below:
class Profile extends React.Component {
constructor(props) {
super(props);
this.state = {
userData: {}
};
}
componentDidMount() {
const userData = this.props.getUserData();
console.log(userData);
this.setState({ userData: userData });
}
render() {
return (
<div className="panel panel-default">
<div className="panel-heading">
<h3 className="panel-title">Hello, {this.state.userData.name || 'World'}</h3>
</div>
<div className="panel-body">
</div>
</div>
)
}
}
class App extends React.Component {
getUserData(){
return {
'name': 'Tom'
}
}
render(){
return (
<Profile getUserData={this.getUserData} />
)
}
};
First of all, you should call getUserData() in componentWillMount rather than componentDidMount to get the data before the component has rendered. And then in the child component use a life cycle method componentWillReceiveProps to check if the child component is getting the right props and updating the state accordingly. It'll be something like this:
componentWillReceiveProps(nextProps) {
if(this.props.userData !== nextProps.userData) {
console.log('condition met');
this.setState({
userData: nextProps.userData
});
}
}
Try this and let me know, I'll help you further.

Clicking link not rendering dom in reactjs

First time data loads properly but when i click filter button like latest or top ajax is passing but view not getting updated. I am not sure what is wrong in my code. I am new to react js.
Here is my example code :-
import React from 'react';
import ReactDOM from 'react-dom';
import axios from 'axios';
import css from './css/bootstrap.css';
//import Search from './Search';
class FetchDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
loading: true,
error: null
};
}
componentDidMount() {
// Remove the 'www.' to cause a CORS error (and see the error state)
axios.get(`https://newsapi.org/v1/articles?source=techcrunch&apiKey=789ea3cd651a49e5ba9fc2061d68138f`)
.then(res => {
//console.log(res.data);
// Transform the raw data by extracting the nested posts
const posts = res.data.articles;
//console.log(posts);
// Update state to trigger a re-render.
// Clear any errors, and turn off the loading indiciator.
this.setState({
posts,
loading: false,
error: null
});
//console.log(this.setState);
})
.catch(err => {
// Something went wrong. Save the error in state and re-render.
this.setState({
loading: false,
error: err
});
});
}
renderLoading() {
return <div>Loading...</div>;
}
renderError() {
return (
<div>
Uh oh: {this.state.error.message}
</div>
);
}
renderPosts() {
if(this.state.error) {
return this.renderError();
}
return (
<div className="row">
<First1/>
{this.state.posts.map(post =>
<div className="col-md-3">
<img src={post.urlToImage} className="img-responsive" />
<h2 key={post.id}>{post.title}</h2>
<p className="lead">
by {post.author}
</p>
<p><span className="glyphicon glyphicon-time"></span> Posted on {post.publishedAt}</p>
<p>{post.description}</p>
</div>
)}
</div>
);
}
render() {
return (
<div>
<h1>Top Stories</h1>
{this.state.loading ?
this.renderLoading()
: this.renderPosts()}
</div>
);
}
}
var First1 = React.createClass({
myClick: function(e){
alert(e.currentTarget.getAttribute("data-city"));
var city = e.currentTarget.getAttribute("data-city");
//alert('Show 1');
axios.get('https://newsapi.org/v1/articles?source=techcrunch&&sortBy='+city+'&apiKey=789ea3cd651a49e5ba9fc2061d68138f')
.then(res => {
//console.log(res.data);
// Transform the raw data by extracting the nested posts
const posts = res.data.articles;
//console.log(posts);
// Update state to trigger a re-render.
// Clear any errors, and turn off the loading indiciator.
//console.log(posts);
this.setState({
posts,
loading: false,
error: null
});
//console.log(this.setState);
})
.catch(err => {
// Something went wrong. Save the error in state and re-render.
this.setState({
loading: false,
error: err
});
});
},
render: function() {
return (<div>
<a onClick={this.myClick} data-city="latest"> Latest</a>
<a onClick={this.myClick} data-city="top"> Top</a>
</div>
);
}
});
// Change the subreddit to anything you like
ReactDOM.render(
<FetchDemo subreddit="reactjs"/>,
document.getElementById('root')
);
Here is link https://jsfiddle.net/69z2wepo/74393/
Issue is first time you are setting the data in parent component, and second time setting the data in child component, you need to update the state of parent component on click of top and latest.
Solution:
Pass a function from parent component and use that function to update the state once you get the response in child component, like this:
In Parent Component:
<First1 _updateState={this._updateState.bind(this)}/>
_updateState(posts){
this.setState({
posts,
loading: false,
error: null
});
}
In Child Component:
myClick: function(e){
....
.then(res => {
this.props._updateState(res.data.articles) //pass data to parent component
})
....
},
Check the fiddle for working solution: https://jsfiddle.net/ndg24fqc/
Note: In 1st component you are using es6 and in 2nd component you are using es5, try to use one thing either es6 or es5.

React — dynamic list <li>'s not accessible in doc.ready, but <ul> is?

I built a list using a json object loaded into the state.
The li items seem to be inaccessible from the document.ready function. i.e. binding a click function to the li's doesn't work.
However, I can bind to the parent UL which I find odd.
Here is my list component:
import React from 'react';
var Albums = React.createClass({
render() {
return (
<ul id="frames_list">
{this.props.frames.map(function(frames){
return (
<li key={frames.frameId}>
<Link to="/frames" >{frames.frameName}</Link>
</li>
)
}.bind(this))}
</ul>
)
}
});
module.exports = Albums;
Here is my doc.ready code:
$('#frames_list li').click(function() {
alert('hello');
});
Here is the parent component setting state:
import React from 'react';
import SideNavHeader from './SideNavHeader';
import SideNavAccordionMenu from './SideNavAccordionMenu';
import SideNavFooter from './SideNavFooter';
var framesData, albumsData, channelsData;
$.getJSON("/json/frames.json", function(frames){
framesData = frames;
});
$.getJSON("/json/albums.json", function(albums){
albumsData = albums;
});
$.getJSON("/json/channels.json", function(channels){
channelsData = channels;
});
var SideNav = React.createClass({
getInitialState() {
return {
frames: [],
albums: [],
channels: []
};
},
componentDidMount() {
var framesList = [], albumsList = [], channelsList = [];
// Fake an AJAX request with a setTimeout function
setTimeout(function(){
framesList=framesData.frames;
albumsList=albumsData.albums;
channelsList=channelsData.channels;
// Set state when the request completes
this.setState({
frames: framesList,
albums: albumsList,
channels: channelsList
});
}.bind(this), 10);
},
render() {
return (
<section>
<SideNavHeader />
<SideNavAccordionMenu frames={this.state.frames} albums={this.state.albums} channels={this.state.channels} />
<SideNavFooter />
</section>
);
}
});
module.exports = SideNav;
You don't want to be attaching click listeners out in your DOM. React is going to be doing stuff to your DOM, including destroying nodes and creating new ones. This may break your listeners, which are tied to specific nodes, which might go away.
In this case, you probably want to use React's built in onClick attribute in JSX, as seen in the docs.
Try something like:
import React from 'react';
var Albums = React.createClass({
handleClick(event) {
console.log('li was clicked');
},
render() {
return (
<ul id="frames_list">
{this.props.frames.map(function(frames){
return (
<li key={frames.frameId} onClick={this.handleClick}>
<Link to="/frames" >{frames.frameName}</Link>
</li>
)
}.bind(this))}
</ul>
)
}
});
module.exports = Albums;
As an aside, for clarity, I personally like break out the mapping through items into its own function:
import React from 'react';
var Albums = React.createClass({
handleClick(event) {
console.log('li was clicked');
},
renderFrames() {
return this.props.frames.map(frame => {
return (
<li key={frame.frameId} onClick={this.handleClick}>
<Link to="/frames" >{frame.frameName}</Link>
</li>
);
});
},
render() {
return (
<ul id="frames_list">
{this.renderFrames()}
</ul>
)
}
});
module.exports = Albums;
if you really want to use jquery event listener, you have to bind your click event in ComponentDidMount function (in your Albums component)

Using setTimeout with React.js before changing state

I would like to use setTimeout with React.js before changing state in a function. I would like to use it in the onHeaderTyped function. I've tried including the TimerMixin but I received an error (it was undefined). Any other routes?
Typing = React.createClass({
getInitialState: function() {
return { showFirst: true, showSecond: false, showThird: false };
},
onHeaderTyped: function() {
this.setState({showFirst: false});
this.setState({showSecond: true});
},
onSecondTyped: function() {
this.setState({showSecond: false});
this.setState({showThird: true});
},
onThirdTyped: function() {
this.setState({showThird: false});
this.setState({showFirst: true});
},
render: function() {
const docs = '#one';
return (
<div>
<div className="TypistExample">
{this.state.showFirst ? (
<Typist className="TypistExample-header" avgTypingSpeed={100} startDelay={1000}
onTypingDone={this.onHeaderTyped} cursor={{hideWhenDone: true}}>
<h1><a href={docs}>First Stuff</a></h1>
</Typist>
) : null }
{this.state.showSecond ? (
<Typist className="TypistExample-header" avgTypingSpeed={100} startDelay={1000}
onTypingDone={this.onSecondTyped}>
<h1> Some Stuff </h1>
</Typist>
) : null }
{this.state.showThird ? (
<Typist className="TypistExample-header" avgTypingSpeed={100} startDelay={1000}
onTypingDone={this.onThirdTyped}>
<h1> More Stuff </h1>
</Typist>
) : null }
</div>
</div>
);
}
});
As an aside, while running this, I am getting this error:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the undefined component.

Resources