4

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.'});
            }
        });
    }
}
Socrates
  • 8,724
  • 25
  • 66
  • 113

2 Answers2

3

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

  1. 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.
  2. this.setState does cause your component to re-render, giving those TextFields the values of this.textFieldUsername/Password and therefore causing the above error.
  3. .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 = "";
y2bd
  • 5,783
  • 1
  • 22
  • 25
0

Error

You are calling this.props.onClickSave and it is not working.

Reason

When you pass in the prop e.g. <DialogUsersNew onClickSave={/*here*/} ensure that you preserve the this context e.g. by using an arrow function <DialogUsersNew onClickSave={()=>this.something()}

basarat
  • 261,912
  • 58
  • 460
  • 511
  • I think I did what you suggest. I updated the above post by adding the `render()` method. – Socrates Mar 20 '19 at 04:02
  • @Socrates What error are you running into after making this change? The console warning you posted in the OP doesn’t appear to be related to `onClickSave`, but instead the `TextField`s and their `value` prop. – y2bd Mar 20 '19 at 04:10
  • @y2bd The error happens when invoking `this.setState({savingErrorMessage: response.message});`. The method `onClickSave()` is invoked by one of the two buttons in the `render()` method. – Socrates Mar 20 '19 at 04:27