I am trying to close a modal when a user presses outside of the Modal element. Somehow when Dismiss() is called, the state is still the same in the callback.
Why is this happening?
export default class Message extends React.Component {
constructor(props) {
super(props);
this.state = {
id: "",
show: false
};
}
componentDidMount() {
this.props.onRef(this);
}
Show(id) {
this.setState({
id: id,
show: true
});
}
Dismiss() {
this.setState({
id: '',
show: false
}, function (state) {
console.log(state) // undefined
});
}
render() {
if (this.state.show) {
return (
<Modal close={() => this.Dismiss()}>
<h1>{this.state.id}</h1>
</Modal>
);
} else {
return null
}
}
}
Not sure why there's a state argument in your callback, should just be
Dismiss() {
this.setState({
id: '',
show: false
}, function () {
console.log(this.state)
});
}
Yes, that is because this.setState function in React is async. And the new state is only available in the event queue
Here is what I mean:
this.setState({newAdded: "test"});
let youCannotGetIt = this.state.newAdded; // here is undefined, because this.setSate is async
Related
I've got a modal (some sort of custom toast) which shows up on the following class called Details when the showModal variable is set to true.
class Details extends React.Component {
constructor(props) {
super(props);
this.state = {
error: null,
code: null,
publication: null,
loading: false,
circleloading: false,
showModal: false
};
this.setReady = this.setReady.bind(this);
}
handleSendMessage(event, errors) {
event.preventDefault();
let currentComponent = this;
let ownerEmail = this.state.publication.userEmail;
let title = this.state.publication.title;
let emailDTO = JsonService.getJSONParsed(event.target);
emailDTO.ownerEmail = ownerEmail;
emailDTO.title = title;
if (Object.keys(errors).length === 0) {
this.setState({
loading: true,
showModal: false,
});
UserService.sendMessage(emailDTO).then(function(response) {
currentComponent.setState({
loading: false,
showModal: true, // This works fine, but it turns out the modal then gets shown after every render
});
});
}
}
render() {
return (
<div>
<ToastNotification
show={this.state.showModal}
title={t('details.messageTitle')}
information={t('details.messageDetail')}
type='Information'
checkModal={false}
specialCloseModal={false}
/>
<ImageVisualizer
publicationid={query.publicationid}
maxImages={this.state.publication.images}
isFavourite={this.state.publication.favourite}
page='Details'
imageClass='imageSize'
containerClass='img-with-tag mySlides'
nextClass='next-image-details pointer centerArrow'
previousClass='prev-image-details pointer centerArrow'
setReady={this.setReady}
index={0}
/>
</div>
);
}
}
export default withRouter(withTranslation()(Details));
The thing is showModal is set to true after UserService.sendMessage gets called, which is ok.
The modal shows up.
But the problem is whenever a render is triggered by another function or something else the modal is shown again?
How can I set showModal to false after showing up?
Try to setState showModal = false with a setTimeout as follow
if (Object.keys(errors).length === 0) {
this.setState({
loading: true,
showModal: false,
});
UserService.sendMessage(emailDTO).then(function (response) {
currentComponent.setState({
loading: false,
showModal: true, // This works fine, but it turns out the modal then gets shown after every render
});
setTimeout(() => {
currentComponent.setState({
showModal: true,
});
}, 200);
});
}
I have a state object that contains an array of objects:
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
I want to toggle the boolean status from true to false on click event. I built this function as a click handler but it doesn't connect the event into the state change:
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
I'm having a hard time following the control flow of the nested React state change, and how the active event makes the jump from the handler to the state object and vice versa.
The whole component:
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: 'alert', status: false },
{ name: 'calm', status: false },
{ name: 'creative', status: false },
{ name: 'productive', status: false },
{ name: 'relaxed', status: false },
{ name: 'sleepy', status: false },
{ name: 'uplifted', status: false }
]
}
}
buttonToggle = (event) => {
event.persist();
const value = !event.target.value
this.setState( prevState => ({
status: !prevState.status
}))
}
render() {
return (
<div>
{ this.state.feeling.map(
(stateObj, index) => {
return <button
key={ index }
onClick={ this.buttonToggle }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
}
)
}
</div>
)
}
}
In order to solve your problem, you should first send the index of the element that is going to be modified to your toggle function :
onClick = {this.buttonToggle(index)}
Then tweak the function to receive both the index and the event.
Now, to modify your state array, copy it, change the value you are looking for, and put it back in your state :
buttonToggle = index => event => {
event.persist();
const feeling = [...this.state.feeling]; //Copy your array
feeling[index] = !feeling[index];
this.setState({ feeling });
}
You can also use slice to copy your array, or even directly send a mapped array where only one value is changed.
Updating a nested object in a react state object is tricky. You have to get the entire object from the state in a temporary variable, update the value within that variable and then replace the state with the updated variable.
To do that, your buttonToggle function needs to know which button was pressed.
return <button
key={ index }
onClick={ (event) => this.buttonToggle(event, stateObj.name) }
value={ stateObj.status } >
{ stateObj.status.toString() }
</button>
And your buttonToggle function could look like this
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
let newFeeling = [];
for (let index in feeling) {
let feel = feeling[index];
if (feel.name == name) {
feel = {name: feel.name, status: !feel.status};
}
newFeeling.push(feel);
}
this.setState({
feeling: newFeeling,
});
}
Here's a working JSFiddle.
Alternatively, if you don't need to store any more data per feeling than "name" and "status", you could rewrite your component state like this:
feeling: {
alert: false,
calm: false,
creative: false,
etc...
}
And buttonToggle:
buttonToggle = (event, name) => {
event.persist();
let { feeling } = this.state;
feeling[name] = !feeling[name];
this.setState({
feeling
});
}
I think you need to update the whole array when get the event. And it is better to not mutate the existing state. I would recommend the following code
export default class StatePractice extends React.Component {
constructor() {
super();
this.state = {
feeling: [
{ name: "alert", status: false },
{ name: "calm", status: false },
{ name: "creative", status: false },
{ name: "productive", status: false },
{ name: "relaxed", status: false },
{ name: "sleepy", status: false },
{ name: "uplifted", status: false },
],
};
}
buttonToggle = (index, value) => (event) => {
event.persist();
const toUpdate = { ...this.state.feeling[index], status: !value };
const feeling = [...this.state.feeling];
feeling.splice(index, 1, toUpdate);
this.setState({
feeling,
});
};
render() {
return (
<div>
{this.state.feeling.map((stateObj, index) => {
return (
<button
key={index}
onClick={this.buttonToggle(index, stateObj.status)}
value={stateObj.status}
>
{stateObj.status.toString()}
</button>
);
})}
</div>
);
}
}
I need to update some data dynamically which are in array without appending new entry
constructor(props) {
super(props);
this.state = {
data: [],
isMounted: false
}
}
componentDidMount() {
this.setState({ isMounted: true })
}
componentWillUnmount() {
this.setState({ isMounted: false })
}
updateData(msg) {
if(this.state.isMounted){
this.setState(
update(this.state, { data: { $push: [msg] } }),
);
}
}
// updateDate method will trigger dynamically
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?
If you're dispatching actions in component lifecycle methods, you should do it in componentDidMount not willMount. It appears you're triggering an infinite loop blocking the component from mounting.
Try by wrapping your onClick function call as-
onClick={() => user.toggleMenu}
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
}
}
This is the code. No idea as to why there is a problem.
class TeacherForm extends Component {
constructor({ data }) {
super();
this.isUpdatingForm = !! data;
this.state = Object.assign({ ... });
this.handleSubmit = this.handleSubmit.bind(this);
this.removeTeacher = this.removeTeacher.bind(this);
}
handleChange(value, field) {
this.setState({ shouldUpdate: true, [field]: value });
}
handleSubmit(e) {
e.preventDefault();
const { name, subjects, parttime, timing } = this.state;
if (this.isUpdatingForm) {
return update.call({
_id: this.props.data._id,
transaction: { name, subjects, parttime, timing },
}, () => this.setState({ shouldUpdate: false }));
}
return add.call();
}
removeTeacher() {
return remove.call(this.props.data._id);
}
render() {
return (
<form onSubmit={this.handleSubmit}>
...
</form>
);
}
}
The error gets thrown at the handleSubmit method in the callback of update.call. This normally shows up when I call removeTeacher and a list updates and this component unmounts.
It sounds like the callback () => this.setState({ shouldUpdate: false }) is executed after that the component is unmounted. Is that possible? If so, one way to get around that is to replace this part by
return update.call({
_id: this.props.data._id,
transaction: { name, subjects, parttime, timing },
}, () => { !this.unmounted && this.setState({ shouldUpdate: false }); });
and to add
componentWillUnmount() {
this.unmounted = true;
}