jamesdeath123 jamesdeath123 - 1 month ago 6
Javascript Question

ComponentWillMount keeps being called repeately

I ma trying to call the backend api to get the user's profile on page load:

Given the following action:

export const GET_MY_PROFILE_START = 'GET_MY_PROFILE_START';
export const GET_MY_PROFILE_ERROR = 'GET_MY_PROFILE_ERROR';
export const GET_MY_PROFILE_SUCCESS = 'GET_MY_PROFILE_SUCCESS';

let a = 0;
export function getMyProfile() {
a = a+1;
window.console.log("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb");
window.console.log(a);
return dispatch => {
window.console.log("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
dispatch(getMyProfileStart());
$.ajax({
type: 'GET',
url: getMyProfileURL,
contentType: "application/json",
dataType: 'json',
}).done(function(res){
if (!res.values || res.values.count > 0) {
dispatch(getMyProfileSuccess(res.data))
} else {
dispatch(getMyProfileError())
}
}).fail(function(error) {
dispatch(getMyProfileError())
})
}
}

function getMyProfileStart() {
return {
type: GET_MY_PROFILE_START,
}
}

function getMyProfileSuccess(profile) {
return {
type: GET_MY_PROFILE_SUCCESS,
profile: profile,
}
}

function getMyProfileError() {
return {
type: GET_MY_PROFILE_ERROR,
}
}


and following reducer:

import { SET_USER_PROFILE, CLEAR_USER_PROFILE, GET_MY_PROFILE_START, GET_MY_PROFILE_ERROR, GET_MY_PROFILE_SUCCESS } from '../serActions'

export default (state = {
loggedIn: false,
profiledLoading: false,
profile: {},
}, action) => {
switch (action.type) {
case SET_USER_PROFILE:
return {
loggedIn: true,
profiledLoading: false,
profile: {},
}
case CLEAR_USER_PROFILE:
return {
loggedIn: false,
profiledLoading: false,
profile: {},
}
case GET_MY_PROFILE_START:
return {
loggedIn: false,
profiledLoading: true,
profile: {},
}
case GET_MY_PROFILE_ERROR:
return {
loggedIn: true,
profiledLoaded: false,
profile: {},
}
case GET_MY_PROFILE_SUCCESS:
return {
loggedIn: true,
profiledLoading: false,
profile: action.profile,
}
default:
return state
}
}


and the following component:

class AvatarButton extends Component {
componentWillMount() {
window.console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
this.props.dispatch(getMyProfile())
}

render() {
//const { profile, toggleMenu, showHideMenu } = this.props
const { user } = this.props
window.console.log(user);
return (
<a className="user-button nav-button avatar-button"
onClick={user.toggleMenu}
onBlur={this.hideMenu.bind(this)}
>
<i className="glyphicon glyphicon-user"/>
</a>
)
}

hideMenu() {
this.props.user.showHideMenu(false)
}
}

AvatarButton.propTypes = {
toggleMenu: PropTypes.func.isRequired,
showHideMenu: PropTypes.func.isRequired,
}

function select(state) {
return {
user: state.user,
}
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(AvatarButton)


This Component is used in:

class UserNavContainer extends Component {
constructor(props) {
super(props)
this.state = {
displayMenu: false,
}
}
render() {
const { dispatch, user, pathname } = this.props
if (!user.loggedIn)
return (
<LoginButton pathname={pathname}/>
)

return (
<div className="user-nav">
<AvatarButton
toggleMenu={this.toggleMenu.bind(this)}
showHideMenu={this.showHideMenu.bind(this)}
/>
<UserMenu
visible={this.state.displayMenu}
logoutHandler={this.logout.bind(this)}
hideMenu={this.showHideMenu.bind(this, false)}
/>
</div>
)
}

logout() {
window.location = "some url"
}

toggleMenu() {
this.showHideMenu(!this.state.displayMenu)
}

showHideMenu(show) {
this.setState({
displayMenu: show,
})
}
}

function select(state) {
return {
user: state.user,
}
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(UserNavContainer)


and finally the top level component that is using UserNavContainer:

class AppHandler extends Component {
componentWillMount() {
authUser(this.authSuccess.bind(this), this.authFail.bind(this))
}

authSuccess() {
this.props.dispatch(login())
}

authFail() {
this.props.dispatch(logout())
}

render() {
window.console.log("renderrenderrenderrenderrenderrenderrenderrenderrenderrender");
return (
<div className="app-container">
<div className="top-nav row">
<div className="col-md-8 col-md-offset-2 col-sm-10 col-sm-offset-1 col-xs-12">
<BackButton
pathname={this.props.location.pathname}
/>
<UserNavContainer
pathname={this.props.location.pathname}
/>
<div className="header">
<img src="https://www.wewherego.com/img/logo/logo_wherego.png"/>
<h1>{this.props.pageHeader}</h1>
</div>
</div>
</div>
<ReactCSSTransitionGroup
transitionName="example"
transitionEnterTimeout={1000}
transitionLeaveTimeout={1000}
>
{React.cloneElement(this.props.children, {
key: this.props.location.pathname,
})}
</ReactCSSTransitionGroup>
</div>
)
}
}

function select(state) {
return {
selectedCity: state.selectedCity,
pageHeader: state.pageHeader,
}
}

// Wrap the component to inject dispatch and state into it
export default connect(select)(AppHandler)


In the AppHandler, it is calling the following methods in the ComponentWillMount:

export function authUser(loginCb, logoutCb) {
const data = readCookie('something')

if (!data) {
logoutCb()
} else {
loginCb()
}
return


}

export function signoutUser() {
//clean up some stuff
}

When I am opening the page, I can only see 1 line of
renderrenderrenderrenderrenderrenderrenderrenderrenderrenderrender

but I see the log keeps printing:

UserNavContainerUserNavContainerUserNavContainerUserNavContainerUserNavContainerUserNavContainer
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
1057
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
1058
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
1059
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa


So apparently the componentWillMount method, for some reason, is kept being called, and the a in getMyProfile() now goes to 1059(and still counting).

I have no idea why this would happen?

Answer

The real problem is that in the components, they are dispatching SET_USER_PROFILE event, which changes the loggedIn variable to true, but then the app dispatches GET_MY_PROFILE_START which changes the loggedIn variable to false. This in return triggers the new cycle and moves loggedIn to true, so the loop keeps going.

The right way to do it is that in the reducer, instead of setting variables, use object.assign to change the values of the variable. So it really should be:

export default (state = {
    loggedIn: false,
    profiledLoading: false,
    profile: {},
}, action) => {
    switch (action.type) {
        case GET_MY_PROFILE_START:
            return Object.assign({}, state, {
                loggedIn: true,
                profiledLoading: true,
            })
        case CLEAR_USER_PROFILE:
            return Object.assign({}, state, {
                loggedIn: false,
            })
        case GET_MY_PROFILE_ERROR:
            return Object.assign({}, state, {
                profiledLoaded: false,
                profile: {},
            })
        case GET_MY_PROFILE_SUCCESS:
            return Object.assign({}, state, {
                profiledLoading: false,
                profile: action.profile,
            })
        default:
            return state
    }
}
Comments