RFR Section 12 – Registration Form Validation


React

Updated Mar 14th, 2022

Table of Contents

Course Summary

Chapter Details

Ch. 66 – Improve Registration Form

We have the very basics for the registration form, but we would like to handle for situations like a username that’s already taken, a special character in the username, etc. And we want to show real-time error messages for these certain cases.

We also want an auto-redirect after successful registration.

In to the “HomeGuest.js” component. Remove the “useState” lines since we are going to be dealing with more complex state. Also delete everything inside the “handleSubmit” function except “e.preventDefault()” and remove the “async” keyword.

Let’s start rebuilding things by bringing in “useImmerReducer” for managing state in this component.

Just inside of the overall “HomeGuest” function, set up our initial state by creating a constant variable named “initialState” set to an object that has a “username” propertiesm which is an object itself, such as “value,” “hasErrors,” and “message” and “isUnique” and “checkCount.”

Duplicate this twice for both “email” and “password.”

Password does not need the “isUnique” or “checkCount” properties.

Add a fourth property for “submitCount” and set it to zero.

const initialState = {

  username: {
    value: "",
    hasErrors: false,
    message: "",
    isUnique: false,
    checkCount: 0
  },

  email: {
    value: "",
    hasErrors: false,
    message: "",
    isUnique: false,
    checkCount: 0
  },

  password: {
    value: "",
    hasErrors: false,
    message: ""

  },

  submitCount: 0

}

Now that we have initial state add a reducer function. List out skeleton for different cases.

Note: We initially juts list out the cases and then fill out throughout the rest of this section.

function ourReducer(draft, action) {
  switch (action.type) {

    case "usernameImmediately":
      draft.username.hasErrors = false
      draft.username.value = action.value
      if (draft.username.value.length > 30) {
        draft.username.hasErrors = true
        draft.username.message = "Username cannot exceed 30 characters."
      }
      return

    case "usernameAfterDelay":
    return

    case "usernameUniqueResults":
    return

    case "emailImmediately":
      draft.email.hasErrors = false
      draft.email.value = action.value
      return

    case "emailAfterDelay":
    return

    case "emailUniqueResults":
    return

    case "passwordImmediately":
      draft.password.hasErrors = false
      draft.password.value = action.value
      return

    case "passwordAfterDelay":
    return

    case "submitForm":
    return

  }
}

Initiate “state” and “dispatch” with “useImmerReducer,”

const [state, dispatch] = useImmerReducer(ourReducer, initialState)

Hollow out the “onChange” on the input for the username

onChange={e => dispatch({type: "usernameImmediately, value: e.target.value})}

Do the same for the email and password fields as well

Fill out the “case” for “usernameImmediately” and copy into the other two as well. We need to store the value in state via draft.username.value = action.” Above this line, also add a “draft.username.hasErrors” and set to false. Copy these lines into “emailImmediately” and “passwordImmediately” “cases” and update the necessary text.

Now that the values are stored in state, we are ready to check against validation rules.

In the “usernameImmediately case,” set up an if statement to handle more than 30 characters.

Leverage package to have the error message animate and fade into and out of view by importing “{CSSTransition}” from “react-transition-group.” Find the input for the username and on a new line of code, right below the input, add “CSSTRANSITION” tags. Inside include a “div” element that outputs the error message using “{state.username.message}.” On the actual opening “CSSTransition” tag, we want to give it some logic to say when this element shoult exist should or not in the DOM by giving it an “in” prop that should be a “boolean” value, “{state.username.hasErrors}.” Also on opening tag, give it a “timeout” prop set to “{330},” a prop of “classNames,” set to blank, and an “unmountOnExit” prop.

<CSSTransition in={{state.username.hasErrors}} timeout={330} classnames="liveValidateMessage" unmountOnExit>
  <div className="liveValidateMessage">"Error Message Text Here."</div>
</CSSTransition>

Note that the “classNames” prop is plural when using the “CSSTransition” tags.

Use a regular expression for only accepting alphanumeric characters for the username

if (draft.username.value && !/^([a-zA-Z0-9]+)$/.test(draft.username.value)) {
  draft.hasErrors = true
  draft.message = "Username can only contain letters and numbers."
}

Ch. 67 – Finish Part 1

To this point, the user will get an error if the username is greater than 30 characters and is non-alphanumeric. These are messages the user should see immediately.

We also want to show an error if the username entered is less than 4 characters, but we need to give the user a chance to finish typing before running our logic.

Set up a delay timer to wait around 800 milliseconds for running validation logic.

In “HomeGuest.js” file, set up a “useEffect” that watches “state.username.value” for changes. Leverage “setTimeout” and “clearTimeout” in cleanup function.

useEffect(() => {
  if (state.username.value) { // makes sure it is not blank
    delay = setTimeout(() => {
      disptach({type: "usernameAfterDelay"})
    }, 800)
    return () => clearTimeout(delay)
  }
}, [state.username.value])

Go into the “case” and set up an “if statement” to check if the value is less than 4 characters long.

case "usernameAfterDelay":
if (draft.username.value.length < 4) {
  draft.username.hasErrors = true
  draft.username.message = "Usernames must be at least 4 characters."
}
return

We also set up another delayed rule to see if the username is unique. We wouldn’t want to flood our server with requests after every keystroke. So see if there are no existing errors, and if there’s no errors, increment the “checkCount” to trigger the server request to check the database for existing username. Then we set up a “useEffect” to watch for changes to the “checkCount” state.

case "usernameAfterDelay":
if (draft.username.value.length < 4) {
  draft.username.hasErrors = true
  draft.username.message = "Usernames must be at least 4 characters."
}
if (!draft.username.hasErrors) {
  draft.username.checkCount++
}
return

Note: The idea here is that if there’s already something wrong with the username, obviously it won’t exist in our database, so don’t bother sending a request.

We borrow the “useEffect” from the “Search.js” file so we don’t have to start from scratch:

useEffect(() => {
  if (state.username.checkCount > 0) {
    const ourRequest = Axios.CancelToken.source()
    async function checkUsername() {
    try {
      const response = await Axios.post('/doesUsernameExist', {username: state.username.value}, {cancelToken: ourRequest.token})
      diapcth({type: "usernameUniqueResults", value: response.data})
    } catch (e) {
      console.log(e)
    }
    }
    checkUsername()
    return () => ourRequest.cancel()
  }
}, [state.username.checkCount])

Now we fill out the “case” of “usernameUniqueResults” with the help of an “if/else” statement.

case "usernameUniqueResults":
  if (action.value) {
    draft.username.hasErrors = true
    draft.username.isUnique = false
    draft.username.message = "That username is already taken."
  } else {
    draft.username.isUnique = true
  }
  return

Begin working on the “email” and “password” fields and start by setting up the delay/timer by duplicating the necessary “useEffect” and tweaking the code. We then fill out some validation logic. We don’t need anything in the “case” of “emailImmediately” but in “emailAfterDelay,” add an “if statement” with regular expression to make sure it is matches the basic pattern for an email.

if (!/^\S+@\S+$/.test(draft.email.value)) {
  draft.email.hasErrors = true
  draft.email.message = "Enter a valid email address."
}

We also want to check to see that the email is unique by setting up another if statement and duplicating/tweaking the “useEffect” from the username code.

if (!draft.email.hasErrors) {
  draft.email.checkCount++
}

In “emailUniqueResults” use an “if/else” statement to handle.

In the “case” of “passwordImmediately,” make sure the password cannot exceed 50 characters

if (draft.password.value.length > 50) {
  draft.password.hasErrors = true
  draft.password.message = "Password cannot exceed 50 characters."
}

In the “case” of “passwordAfterDelay,” make sure the password is at least 8 characters long.

if (draft.password.value.length < 8) {
  draft.password.hasErrors = true
  draft.password.message = "Password must be at least 8 characters."
}

Go into the JSX and make sure you are showing the red warning “div” element for the email and password fields. Duplicate and tweak the “CSSTransition” tags and paste just below the inputs.

Ch. 68 – Quick Note: There was an error with one of the “if statements” in the course video. Updated.

Ch. 69 – Finish Part 2

When the tries to submit the form, we need to run all of our validation checks. We need to make sure we are happy with values before even bothering the server. In the “handleSubmit” function, prevent the default and then call a bunch of dispatches.

function handleSubmit(e) {
  e.preventDefault()
  dispatch({type:"usernameImmediately", value: state.username.value})
  dispatch({type:"usernameAfterDelay", value: state.username.value})
  dispatch({type:"emailImmediately", value: state.email.value})
  dispatch({type:"emailAfterDelay", value: state.email.value})
  dispatch({type:"passwordImmediately", value: state.password.value})
  dispatch({type:"passwordAfterDelay", value: state.password.value})
  dispatch({type: "submitForm"})
}

When a user clicks submit and we want to run our logic, we don’t want to run the “Axios isUnique” checks, because if the user left either of these blank, the minimum length or other validation rules will catch that when the user clicks submit. If they actually did enter a name, as soon as they stopped typing, that is when we check we if that is unique, and so we wouldn’t want to run these “unique checks” again a second time if the user clicks submit.

So back in the “handleSubmit” function, in the dispatch with the “type” of “userNameAfterDelay,” give the object passed into dispatch a 3rd property called “noRequest” and set it to true. Do the same for the “emailAfterDelay” dispatch.

Go into both of these “cases” and account for the “noRequest” property by tweaking the existing “if statement” condition

if ( !draft.username.hasErrors && !action.noRequest) {
  draft.username.checkCount++
}

By doing it this way you don’t bother checking “isUnique” if the user is submitting the form without actually typing in the field.

Go into the “case” of “submitForm,” and set up and “if statement” to make sure you are happy with the current values. Check that none of the fields have errors, and the username and email are unique.

case "submitForm":
  if (
    !draft.username.hasErrors &&
     draft.username.isUnique &&
    !draft.email.hasErrors &&
     draft.email.isUnique &&
    !draft.password.hasErrors
  ) {
  draft.submitCount++
  }
  return

Set up a “useEffect” that watches the “submitCount” to actually submit the form to the server at the appropriate time.

Duplicate the “useEffect” that sends a request to “/doesUsernameExist” and tweak a bit. Update the dependency array to watch for “state.submitCount” and URL should be “/register” and add email and password values to the data being sent to server. When the request finishes, remove existing dispatch, and replace with an “app-wide or global” dispatch which we have to bring in first. So bring in “app-wide dispatch” using the process mentioned earlier in course.

appDispatch({type: "login", data: response.data})
appDispatch({type: "flashMessage", value: "Congrats and Welcome."})

Ch. 70 – Quick Flash Message Details

In the “HeaderLoggedOut.js” file, after login request, add a flash message via an “appDispatch.” In the “else block” do the same for a failed login attempt. Jump into the “HeaderLoggedIn.js” file and in the “handleLogOut” function add a flash message for a successful logout.

// in HeaderLoggedOut.js, in the after login dispatch

appDispatch({type: "flashMessage, value: "You have successfully logged in."})

// in the else block

appDispatch({type: "flashMessage, value: "Invalid username/password."})
// in HeaderLoggedIn.js in the "handleLogout" function

appDispatch({type: "flashMessage, value: "You have successfully logged out."})

Ch. 71 – Proactively Check If Token Has Expired

Why do you need to do this? The server validates the token and if you were to visit our site after a long enough time that the token has expired, the browser doesn’t know that. So you spend time creating a new post and then submit the post only to get an error message that your token is expired. You would be very frustrated.

When the component first renders we want to fire off an “Axios” request to check if the token is expired and, if so, redirect the user to login again. So we proactively check if the token is expired and if it is no longer valid, we force the user to be logged out so they need to login again and get a new fresh token.

In the “Main.js” file, just above the JSX, set up a new “useEffect” by duplicating the “useEffect” in the “Search.js” file that sends an “Axios.post()” request to a “/search” URL.

Update the dependency array to be blank. Change the “if condition” to be “if (state.loggedIn)” and change the URL to “/checkToken” and send “{token: state.user.token}” as the data payload.

When the request finishes the server will return with a response of either true or false, so after the “Axios” request, but still within the “try block,” add “if (!response.data),” then send a dispatch with an object that has a property of “type” set to “logout.” Below that show a flash message via another dispatch with a “type” property set to “flashMessage” and “value” property set to “Your session has expired, please login again.”

For testing purposes you can force the token to expire in 30 seconds by going into the back-end API server and in the “userController.js” file, around line number 6, you can change the expiration time and restart the server. The expiration value should be set to like a week, or a month, I think.

Note: I am noticing in this chapter that in the “Main.js” file it’s not “appDispatch” it’s just dispatch. This is specific to the “Main.js” file since the reducer function lives in this file.

Also note: Some other components/files have their own reducer and also just call dispatch since it is a “local” dispatch.

Also Also note: When you bring in “global” or “app-wide” dispatch, by importing the “DispatchContext.js” and/or “StateContext.js” files and setting a constant variable named “appDispatch” or “appState” and set equal to “useContext(DispatchContext)” this constant variable is why we call “appDispatch” instead of just “dispatch.”