2

I'm trying to validate an input field for emailaddresses. The actual validation works, the state updates and everything around it works. However, I noticed a strange behaviour once a false email is validated.

NOTE: I changed the name of my states and attributes to make it more generic and readable.

<TextField
    label = {emailFieldAttributes.label}
    type = {emailFieldAttributes.type}
    onChange = { event => {
        onChangeHandler(event)
    }}
    id = {emailFieldAttributes.id}
    value = {state.value_email}
    InputProps = {{
        autoComplete: 'off',
        inputMode: 'string'
    }}
    error = {state.error.value}
    ref = {ref}
></TextField>
function onChangeHandler(event)
    {
        if (event.target.id.includes('cc'))
        {
            setState({...state, value_cc: event.target.value});
        }
        else if (event.target.id.includes('body'))
        {
            setState({...state, value_body: event.target.value});
        }
        else
        {
            setState({...state, value_email: event.target.value});
        }
        
        if (state.error != initialErrorState
        )
        {
            resetEmailTooltip();
        }
    }
function resetEmailTooltip()
{
    setEmailStateValues(
    {
        ...state,
        error: resetErrorState
    });
}

In the submit buttons' onClick, I run the validation and it works as it updates the state correctly and shows a tooltip. In fact, the onChange works as it's supposed to except for 1 slight undesired behaviour. Whenever a validation does return false and updates the state.error, a tooltip is shown. The next time a change is done on the input field the first event only triggers resetEmailTooltip(); and does not the update the state value.

Stepwise - current behaviour

  1. Add email address with incorrect format: 'test@.test'
  2. Press submit
  3. Validation
  4. Tooltip shown
  5. First change in the inputField by pressing down e.g. 'b' on the keyboard leads to tooltip being removed
  6. 2nd event of pressing down 'b' would lead to 'test@b.test'

Stepwise - desired behaviour

  1. Add email address with incorrect format: 'test@.test'
  2. Press submit
  3. Validation
  4. Tooltip shown
  5. First change in the inputField by pressing down e.g. 'b' on the keyboard leads to tooltip being removed and leads to 'test@b.test'

What I don't understand is why it requires a second event to trigger the state update? Do I have something wrong regarding my thought process when it comes to how onChange is fired?

DragonInTraining
  • 385
  • 1
  • 12
  • Try to print your state on the console. If I understand your problem, it changes even the first time, but in an asynchronous way, so you need another event to visualize it correctly on the page – Giacomo Nov 29 '21 at 11:18
  • *Possible* duplicate of: ["useState set method not reflecting change immediately"](https://stackoverflow.com/questions/54069253/usestate-set-method-not-reflecting-change-immediately) ? – Yoshi Nov 29 '21 at 11:27
  • @Giacomo Hi, it seems that the event itself never gets updated. `event.target.value` never changes on the first event. @Yoshi I looked through that, and it seems to handle the states primarily while I'm more confused about the field itself and as to why the event doesn't update. I understand that console logging the state value immediatelly after setting it will provide me with the older variable, but the event itself should be updated. – DragonInTraining Nov 29 '21 at 12:16
  • Without seeing a more complete *picture* (especially of ``) it's hard to tell whats going on here. But with regards to the linked answer: No it's not mainly about how `console.log`s might *trail behind*. The main take-away is, that you can't update the state via the state updater function and immediately use that updated state. For you that would mean, that `state.error` at the and of `onChangeHandler` **will not** have been affected by the previous `setState` calls. – Yoshi Nov 29 '21 at 13:47
  • 1
    @Yoshi I actually found the problem and im about to answer it. It's a bad/rookie mistake when using 2 setStates after one another. I just couldn't see it until I took a step back and tried a few more things. The event issue was not reoccuring and only happened at times so I was mistaken there as well, regarding your comment about useState not reflecting change steered me in the right direction, thank you! – DragonInTraining Nov 29 '21 at 13:54
  • Why are you setting all of the state every time you update one value? – epascarello Nov 29 '21 at 14:17
  • @epascarello It's because I declared my local state as controlled, without the entirety of state being set, I receive warnings regarding controlled vs incontrolled input. – DragonInTraining Nov 29 '21 at 14:31

1 Answers1

1

I dug a bit more and found that event.target.value was indeed a bit unstable but it wasn't really the cause of my problem. My problem arose from using 2 setStates which can be seen in the code, I should've spotted this sooner but being new to React it took some time to figure it out.

So even though the event actually tries to update the State, the async nature of setState seems to be overwritten by the 2nd setState in the resetEmailTooltip function. I solved it by changing the onChangeHandler and resetEmailTooltip to look like this (Note: I'm not liking the volume of code to handle this, so I will try and rewrite the code later):

function onChangeHandler(event)
{

    if (state.error != initialErrorState )
    {
        resetEmailTooltip(event);
    }
    else
    {
        if (event.target.id.includes('cc'))
        {
            setState({...state, value_cc: event.target.value});
        }
        else if (event.target.id.includes('body'))
        {
            setState({...state, value_body: event.target.value});
        }
        else
        {
            setState({...state, value_email: event.target.value});
        }
    }
}

And

function resetEmailTooltip(event)
{
    if (event.target.id.includes('cc'))
    {
        setState(
        {
            ...state,
            value_cc: event.target.value,
            error: resetErrorState
        });
    }
    else if (event.target.id.includes('body'))
    {
        setState(
        {
            ...state,
            value_body: event.target.value,
            error: resetErrorState
        });
    }
    else
    {
        setState(
        {
            ...state,
            value_email: event.target.value,
            error: resetErrorState
        });
    }
}

The short story is that I update the state with event.target.value in both functions and call one setState instead of both. This ensures that the event.target.value is set and also that I don't need to call both setStates. However, this still feels clunky and I think there's a way to shorten this code even more and get rid of the several if statements and I will look into that.


Update: Since updating the local state with the same value doesn't cause a re-render, I decided to cut out more code and even remove the resetEmailTooltip(event) function. I changed my onChangeHandler(event) to the following piece of code and it works exactly like I want it to and I even got to clean it up a bit. If possible, I will look into having internal conditions in a setState, but I'm not sure if that's allowed. This is the end result for now, hope it helps anyone if they get stuck like I did with the same issue.

function onChangeHandler(event)
{
    const resetErrorState = state.initialErrorState;

    if (event.target.id.includes('cc'))
    {
        setState(
            {
                ...state,
                value_cc: event.target.value,
                error: resetErrorState
            }
        );
    }
    else if (event.target.id.includes('body'))
    {
        setState(
            {
                ...state,
                value_body: event.target.value,
                error: resetErrorState
            }
        );
    }
    else
    {
        setState(
            {
                ...state,
                value_email: event.target.value,
                error: resetErrorState
            }
        );
    }
}
DragonInTraining
  • 385
  • 1
  • 12