Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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><a href="/login">Login</a></li>
                <li><a href="/register">Register</a></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><a href="/logout">Logout</a></li>
            </ul>;
            this.setState({
                user_actions : this.state.user_actions
            });
        }
    },

    render : function(){
        return (
            <nav className="navbar navbar-default">
                <div className="container">
                    <a href="/" className="navbar-brand">Reactor</a>
                    {this.state.user_actions}
                </div>
            </nav>
        );
    }
});
like image 463
Waleed Ahmad Avatar asked Apr 15 '16 13:04

Waleed Ahmad


1 Answers

First of all, I suggest you to reread React.JS documentation, because there are couple of things that need to be noted:

  1. Never mutate this.state directly, use setState method instead. (line: 108, 111, 121, 133, 136, 146)
  2. 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><a href="/login">Login</a></li>
                <li><a href="/register">Register</a></li>
            </ul>);
        } else {
            navItems =  (<ul className="nav navbar-nav navbar-right">
                <li><a href="/logout">Logout</a></li>
            </ul>);
        }

        return (
            <nav className="navbar navbar-default">
                <div className="container">
                    <a href="/" className="navbar-brand">Reactor</a>
                    { navItems }
                </div>
            </nav>
        );
    }
});

Hope it helps!

like image 148
mirza.adipradhana Avatar answered Sep 21 '22 08:09

mirza.adipradhana