2

I'm new to react and toying with a sign-up form. My render method always displays the current state of user.name, user.email and the error attribute is always flagged appropriately.

However, within my bound methods (considerSubmit, validateEmail, etc..) console.log(this.state) outputs my default state, not the current state.

What am I missing here? I thought that .bind(this) would synchronize the state amongst all methods.

import React, {Component} from 'react';
import PropTypes from 'prop-types';
import {withStyles, createStyleSheet} from 'material-ui/styles';
import TextField from 'material-ui/TextField';
import Button from 'material-ui/Button';
import Dialog, {
    DialogActions,
    DialogContent,
    DialogContentText,
    DialogTitle,
} from 'material-ui/Dialog';
import Slide from 'material-ui/transitions/Slide';

const popsicle = require('popsicle');
const styleSheet = createStyleSheet('RegistrationProgress', {
    root: {
        maxWidth: 400,
        flexGrow: 1,
    },
});

class RegistrationProgress extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            user: {
                name: null,
                isNameValid: null,
                email: null,
                isEmailValid: null
            },
            notice: {
                title: null,
                message: null,
                open: false,
            }
        };
    }

    handleRequestClose() {
        let noticeState = this.state.notice;
        noticeState.open = false;
        this.setState({notice: noticeState});
    };

    considerSubmit(event) {
        const isSubmitAction = event.key === 'Enter' || event.type === 'click';
        if (isSubmitAction) {
            let userState = this.state.user;
            let formReady = (userState.isNameValid && userState.isEmailValid);
            if (!formReady) {
                this.showNotice("Hold on a sec!", "Make sure your first and last name is provided as well as a proper email address.");
                return;
            }
            var RegistrationProgress = this;
            var element = document.querySelector('meta[name="csrf-token"]');
            var csrf_token = element && element.getAttribute("content");
            console.log(userState, userState.name,this.state.user)
            popsicle.request({
                method: 'POST',
                url: '/register',
                body: {
                    name: userState.name,
                    email: userState.email,
                    _token: csrf_token
                },
                headers: {
                    'X-XSRF-TOKEN': csrf_token
                }
            })
                .use(popsicle.plugins.parse('json'))
                .then(function (res) {
                    console.log(res.status) // => 200
                    console.log(res.body) //=> { ... }
                    console.log(res.get('Content-Type')) //=> 'application/json'
                    RegistrationProgress.showNotice("Yeehaw!", "Account created! Confirm your email to login.");
                })
                .catch(function(error){
                    RegistrationProgress.showNotice("Uh-oh.", "Looks like our server hiccuped when handling your request. Try again.")
                });
        }
        return event;
    }

    showNotice(title = "Whoa!", message) {
        this.setState({
            notice: {
                title: title,
                message: message,
                open: true
            }
        })
    }

    validateName(event) {
        const nameRule = /^(([A-Za-z]+[\-\']?)*([A-Za-z]+)?\s)+([A-Za-z]+[\-\']?)*([A-Za-z]+)?$/;
        let registerName = (event.target.value).trim();
        let userState = this.state.user;
        userState.isNameValid = nameRule.test(registerName);
        console.log(userState)
        this.setState({user: userState})
    }

    validateEmail(event) {
        const emailRule = /^([a-zA-Z0-9_\-\.]+)@([a-zA-Z0-9_\-\.]+)\.([a-zA-Z]{2,5})$/;
        let registerEmail = (event.target.value).trim();
        let userState = this.state.user;
        userState.isEmailValid = emailRule.test(registerEmail);
        this.setState({
            user: userState
        })
    }

    render() {
        const classes = this.props.classes;
        return (
            <div className="register-form" onKeyPress={this.considerSubmit.bind(this)}>
                <TextField id="name" name="name" label="Full Name" type="text" defaultValue={this.state.user.name}
                           className={classes.input}
                           error={RegistrationProgress.getErrorState(this.state.user.isNameValid)}
                           helperText="" onChange={(event) => this.validateName(event)} marginForm
                />
                <br/>
                <TextField id="email" name="email" label="Email" type="email" defaultValue={this.state.user.email}
                           className={classes.input}
                           error={RegistrationProgress.getErrorState(this.state.user.isEmailValid)}
                           helperText="" onChange={(event) => this.validateEmail(event)} marginForm
                />
                <br />
                <Button raised color="primary" className={'register-button ' + classes.button}
                        onClick={(event) => this.considerSubmit(event)}>
                    Sign Up
                </Button>
                <Dialog open={this.state.notice.open} transition={Slide}
                        onRequestClose={this.handleRequestClose.bind(this)}>
                    <DialogTitle>
                        {this.state.notice.title}
                    </DialogTitle>
                    <DialogContent>
                        <DialogContentText>
                            {this.state.notice.message}
                        </DialogContentText>
                    </DialogContent>
                    <DialogActions>
                        <Button onClick={this.handleRequestClose.bind(this)} color="primary">
                            Got it!
                        </Button>
                    </DialogActions>
                </Dialog>
            </div>
        );
    }

    static getErrorState(value) {
        return (!value && value !== null);
    }
}

RegistrationProgress.propTypes = {
    classes: PropTypes.object.isRequired,
};

export default withStyles(styleSheet)(RegistrationProgress);
TylersSN
  • 1,391
  • 3
  • 21
  • 39
  • I would suggest you to be consistent in using fat arrow or bind for binding the function, you could easily replace, `this.handleRequestClose.bind(this)` with `() => this.handleRequestClose()`. Also instead of `let userState = this.state.user;` use `let userState = {...this.state.user};` to prevent direct mutation of state, – Shubham Khatri Jul 17 '17 at 15:24
  • 1
    Also I suppose the problem could be the same as https://stackoverflow.com/questions/41278385/this-setstate-doesnt-mutate-state-immediately/41278440#41278440. Also refer https://stackoverflow.com/questions/42811882/what-is-the-meaning-of-this-syntax-x-in-reactjs/42811937#42811937 if you wanna know what `let userState = {...this.state.user};` means – Shubham Khatri Jul 17 '17 at 15:25
  • Thanks for the feedback. I'll definitely consider the suggestions. Please see my comment in response to @Finbarr-ob's answer. – TylersSN Jul 17 '17 at 16:14

1 Answers1

0

Your handlers for the components are just binding 'this' to the handler methods but not invoking them.

You can bind 'this' in your constructor, or make your handler functions as 'fat arrow' functions (this is bound automatically then). Then, be sure to set the handlers correctly in your components:

constructor:

constructor(props) {
    super(props);
    this.state = {
        user: {
            name: null,
            isNameValid: null,
            email: null,
            isEmailValid: null
        },
        notice: {
            title: null,
            message: null,
            open: false,
        }
    };
    this.considerSubmit.bind(this);
    this.handleRequestClose.bind(this);
    this.handleRequestClose.bind(this);
}

render:

render() {
    const classes = this.props.classes;
    return (
        <div className="register-form" onKeyPress={this.considerSubmit()}>
            <TextField id="name" name="name" label="Full Name" type="text" defaultValue={this.state.user.name}
                       className={classes.input}
                       error={RegistrationProgress.getErrorState(this.state.user.isNameValid)}
                       helperText="" onChange={(event) => this.validateName(event)} marginForm
            />
            <br/>
            <TextField id="email" name="email" label="Email" type="email" defaultValue={this.state.user.email}
                       className={classes.input}
                       error={RegistrationProgress.getErrorState(this.state.user.isEmailValid)}
                       helperText="" onChange={(event) => this.validateEmail(event)} marginForm
            />
            <br />
            <Button raised color="primary" className={'register-button ' + classes.button}
                    onClick={(event) => this.considerSubmit(event)}>
                Sign Up
            </Button>
            <Dialog open={this.state.notice.open} transition={Slide}
                    onRequestClose={this.handleRequestClose()}>
                <DialogTitle>
                    {this.state.notice.title}
                </DialogTitle>
                <DialogContent>
                    <DialogContentText>
                        {this.state.notice.message}
                    </DialogContentText>
                </DialogContent>
                <DialogActions>
                    <Button onClick={this.handleRequestClose())} color="primary">
                        Got it!
                    </Button>
                </DialogActions>
            </Dialog>
        </div>
    );
}
Finbarr O'B
  • 1,330
  • 1
  • 11
  • 16
  • Thanks for your suggestion. I gave this a shot by adding each method to the constructor with a bind, however `console.log(userState)` within validateName still outputs null for name and email objects despite them syncing correctly within render. I suppose I should dig deeper into "invoking methods" – TylersSN Jul 17 '17 at 15:43
  • Why this wasn't obvious, I don't know. I was behaving as if defaultValue was keeping the name state synchronized. validateName() was only validating and setting isNameValid to true or false. Nothing was ever setting the state of the name. – TylersSN Jul 17 '17 at 16:11