I have an error when setting the state within catch of a Promise. In the example below the Promise's catch
is within the onClickSave()
method. I believe I get the error, because I misunderstand the this
context I am in. Here I want to use this
to address the contents of the class DialogUsersNewProps
. Coming from Java, where this
behaves a bit different, I already got confused in JavaScript in the past. What must I do to set the state from within the catch
of the rejected Promise?
Error from browser console:
/home/myuser/Documents/myprog/administration2/node_modules/react-dom/cjs/react-dom.development.js:506 Warning: A component is changing an uncontrolled input of type text to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa). Decide between using a controlled or uncontrolled input element for the lifetime of the component.
in input (created by InputBase)
in div (created by InputBase)
in InputBase (created by Context.Consumer)
in WithFormControlContext(InputBase) (created by WithStyles(WithFormControlContext(InputBase)))
in WithStyles(WithFormControlContext(InputBase)) (created by Input)
in Input (created by WithStyles(Input))
in WithStyles(Input) (created by TextField)
in div (created by FormControl)
in FormControl (created by WithStyles(FormControl))
in WithStyles(FormControl) (created by TextField)
in TextField (created by DialogUsersNew)
in div (created by DialogContent)
in DialogContent (created by WithStyles(DialogContent))
in WithStyles(DialogContent) (created by DialogUsersNew)
in div (created by Paper)
in Paper (created by WithStyles(Paper))
in WithStyles(Paper) (created by DialogUsersNew)
in DialogUsersNew (created by DisplayUsers)
in DisplayUsers (created by DisplayChoice)
in DisplayChoice (created by DisplayMain)
in main (created by DisplayMain)
in div (created by DisplayMain)
in div (created by DisplayMain)
in DisplayMain (created by App)
in App
in AppContainer
Failing TypeScript class:
import {
Button,
DialogActions,
DialogContent,
Paper,
TextField,
Typography,
} from '@material-ui/core';
import * as React from 'react';
import { User } from '../../../data/model/user';
import { AddNewUserResponse } from '../../../data/services/add-new-user-response';
import { DialogMessage } from '../../dialogs/dialog-message';
export interface DialogUsersNewProps {
onClickSave(user: User): Promise<AddNewUserResponse>;
onClickAbort(): void;
}
export interface DialogUsersNewState {
savingErrorMessage: string;
}
export class DialogUsersNew extends React.Component<DialogUsersNewProps, DialogUsersNewState> {
private textFieldUsername: string;
private textFieldPassword: string;
public constructor(props: any) {
super(props);
this.state = {
savingErrorMessage: '',
};
}
public render() {
return <Paper>
{this.state.savingErrorMessage !== '' &&
<DialogMessage title='Saving error' content={this.state.savingErrorMessage} />
}
<DialogContent>
<Typography variant='h5'>New user</Typography>
<TextField label='Username'
value={this.textFieldUsername}
className='w-100 fieldMargin'
onChange={(e: any) => this.onChangeTextFieldUsername(e.target.value)}
margin='normal'/>
<TextField label='Password'
type='password'
value={this.textFieldPassword}
className='w-100 fieldMargin'
onChange={(e: any) => this.onChangeTextFieldPassword(e.target.value)}
margin='normal'/>
<DialogActions>
<Button onClick={() => this.props.onClickAbort()} color='primary'>Abort</Button>
<Button onClick={() => this.onClickSave()} color='primary' variant='contained'>Save</Button>
</DialogActions>
</DialogContent>
</Paper>;
}
private getUser(): User {
// Generate new user based on props and textfields.
return {
password: this.textFieldPassword,
username: this.textFieldUsername,
};
}
private onChangeTextFieldUsername(content: string) {
// Save textbox change.
this.textFieldUsername = content;
}
private onChangeTextFieldPassword(content: string) {
// Save textbox change.
this.textFieldPassword = content;
}
private onClickSave() {
// Send click save event to parent.
this.props.onClickSave(this.getUser()).then((response: AddNewUserResponse) => {
// Check if success has failed.
if (!response.success) {
// Save message in state.
if (response.message) {
this.setState({savingErrorMessage: response.message});
} else {
this.setState({savingErrorMessage: 'Undefined error.'});
}
}
}).catch((response: AddNewUserResponse) => {
// Save message in state.
if (response.message) {
this.setState({savingErrorMessage: response.message});
} else {
this.setState({savingErrorMessage: 'Undefined error.'});
}
});
}
}
Alright, two issues happening here.
First, when using <input />
components (or components that use them under the hood like your TextField
), if you want to control their value (value={this.foobar}
) you have to always control the value. Where you are running into an issue is that this.textFieldUsername/Password
is initially undefined
, which will lead to the error as described here: React - changing an uncontrolled input
Secondly, the reason that this is not occuring until you click on the save button is because
this.textFieldUsername/Password
is not in React state, which means that when it changes it does not cause your Component to re-render, delaying the occurrence of the error.this.setState
does cause your component to re-render, giving those TextField
s the values of this.textFieldUsername/Password
and therefore causing the above error..catch
blocks will catch errors in the pairing .then
block. Since this.setState
in your .then
leads to the above error happening within render()
, that error is bubbled up back to your Promise and lands you unhelpfully in the .catch
block. I cannot find an SO post about this, so here is an MVP proving it is the case: https://codesandbox.io/s/2wzvj6p7v0. Easiest fix would be
private textFieldUsername: string = "";
private textFieldPassword: string = "";
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With