React in 60 seconds: form validation





Good day, friends!



In this small tutorial, I want to show you an example of client-server form validation.



The client will be implemented in React, the server in Express.



We will not reinvent the wheel, but use ready-made solutions: react-hook-form will be used to validate the form on the client side (+: hooks are used, Russian), and on the server side - express-validator . Styled-components (CSS-in-JS or All-in-JS, given JSX)



will be used for styling . The source code for the example is here .







You can play with the code here .



Without further preamble.



Customer



Create a project using create-react-app :



yarn create react-app form-validation
# 
npm init react-app form-validation
# 
npx create-react-app form-validation

      
      





In the future, I will use yarn to install dependencies and run commands.



Project structure after removing unnecessary files:



public
  index.html
src
  App.js
  index.js
  styles.js
server.js
...

      
      





Install dependencies:



#  
yarn add styled-components react-hook-form

#   ( )
yarn add express express-validator cors

#   (  )
yarn add -D nodemon

#    
yarn add concurrently

      
      





Since styled-components can't import fonts, we'll have to add them to public / index.html:



<head>
  ...
  <link rel="preconnect" href="https://fonts.gstatic.com" />
  <link
    href="https://fonts.googleapis.com/css2?family=Comfortaa&display=swap"
    rel="stylesheet"
  />
</head>

      
      





Our form will have three fields: username, email address and password. Conditions that data must satisfy:



  • Name

    • from 2 to 10 characters
    • Cyrillic


  • Email

    • no special requirements


  • Password

    • 8-12 characters
    • Latin: letters in any case, numbers, underscore and hyphen




Let's start with styling (src / styles.js; for syntax highlighting I use the VSCode extension vscode-styled-components):



//  
import styled, { createGlobalStyle } from 'styled-components'

//  
const GlobalStyle = createGlobalStyle`
  body {
    margin: 0;
    min-height: 100vh;
    display: grid;
    place-items: center;
    background-color: #1c1c1c;
    font-family: 'Comfortaa', cursive;
    font-size: 14px;
    letter-spacing: 1px;
    color: #f0f0f0;
  }
`

// 
const StyledTitle = styled.h1`
  margin: 1em;
  color: orange;
`

// 
const StyledForm = styled.form`
  margin: 0 auto;
  width: 320px;
  font-size: 1.2em;
  text-align: center;
`

// 
const Label = styled.label`
  margin: 0.5em;
  display: grid;
  grid-template-columns: 1fr 2fr;
  align-items: center;
  text-align: left;
`

//     
const BaseInput = styled.input`
  padding: 0.5em 0.75em;
  font-family: inherit;
  font-size: 0.9em;
  letter-spacing: 1px;
  outline: none;
  border: none;
  border-radius: 4px;
`

//  
const RegularInput = styled(BaseInput)`
  background-color: #f0f0f0;
  box-shadow: inset 0 0 2px orange;

  &:focus {
    background-color: #1c1c1c;
    color: #f0f0f0;
    box-shadow: inset 0 0 4px yellow;
  }
`

//      
const SubmitInput = styled(BaseInput)`
  margin: 1em 0.5em;
  background-image: linear-gradient(yellow, orange);
  cursor: pointer;

  &:active {
    box-shadow: inset 0 1px 3px #1c1c1c;
  }
`

//    
const BaseText = styled.p`
  font-size: 1.1em;
  text-align: center;
  text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
`

//   
const ErrorText = styled(BaseText)`
  font-size: ${(props) => (props.small ? '0.8em' : '1.1em')};
  color: red;
`

//   
const SuccessText = styled(BaseText)`
  color: green;
`

//   
export {
  GlobalStyle,
  StyledTitle,
  StyledForm,
  Label,
  RegularInput,
  SubmitInput,
  ErrorText,
  SuccessText
}

      
      





Let's import and include global styles in src / index.js:



import React from 'react'
import ReactDOM from 'react-dom'

//   
import { GlobalStyle } from './styles'

import App from './App'

ReactDOM.render(
  <React.StrictMode>
    {/*    */}
    <GlobalStyle />
    <App />
  </React.StrictMode>,
  document.getElementById('root')
)

      
      





Go to the main client file (src / App.js):



import { useState } from 'react'
//     
import { useForm } from 'react-hook-form'

//   
import {
  StyledTitle,
  StyledForm,
  Label,
  RegularInput,
  SubmitInput,
  ErrorText,
  SuccessText
} from './styles'

//  
function Title() {
  return <StyledTitle> </StyledTitle>
}

//  
function Form() {
  //   
  const [result, setResult] = useState({
    message: '',
    success: false
  })

  //   :
  //   
  //     
  const { register, errors, handleSubmit } = useForm()

  //  
  const validators = {
    required: '   '
  }

  //   
  async function onSubmit(values) {
    console.log(values)

    const response = await fetch('http://localhost:5000/server', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(values)
    })

    const result = await response.json()

    //  
    setResult({
      message: result,
      success: response.ok
    })
  }

  //           
  function onClick() {
    window.location.reload()
  }

  return (
    <>
      <StyledForm onSubmit={handleSubmit(onSubmit)}>
        <Label>
          :
          <RegularInput
            type='text'
            name='name'
            //   
            //   
            ref={register({
              ...validators,
              minLength: {
                value: 2,
                message: '   '
              },
              maxLength: {
                value: 10,
                message: '   '
              },
              pattern: {
                value: /[-]{2,10}/i,
                message: ' '
              }
            })}
            defaultValue=''
          />
        </Label>
        {/*  */}
        <ErrorText small>{errors.name && errors.name.message}</ErrorText>

        <Label>
          Email:
          <RegularInput
            type='email'
            name='email'
            ref={register({
              ...validators,
              pattern: {
                value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
                message: '   '
              }
            })}
            defaultValue='email@example.com'
          />
        </Label>
        <ErrorText small>{errors.email && errors.email.message}</ErrorText>

        <Label>
          :
          <RegularInput
            type='password'
            name='password'
            ref={register({
              ...validators,
              pattern: {
                value: /^[A-Z0-9_-]{8,12}$/i,
                message:
                  ' 8  12 : , ,    '
              }
            })}
            defaultValue='password'
          />
        </Label>
        <ErrorText small>
          {errors.password && errors.password.message}
        </ErrorText>

        <SubmitInput type='submit' defaultValue='' />

        {/*     "as",    ""      */}
        <SubmitInput as='button' onClick={onClick}>
          
        </SubmitInput>
      </StyledForm>

      {/*    */}
      {result.success ? (
        <SuccessText>{result.message}</SuccessText>
      ) : (
        <ErrorText>{result.message}</ErrorText>
      )}
    </>
  )
}

export default function App() {
  return (
    <>
      <Title />
      <Form />
    </>
  )
}

      
      





The register () method of the useForm () hook supports all the attributes of the input tag. A complete list of such attributes . In the case of a name, we could limit ourselves to a regular expression.



Start the server for the client using yarn start and test the form:







Great. Client-side validation works as expected. But you can always turn it off. Therefore, validation on the server is needed.



Server



Let's start implementing the server (server.js):



const express = require('express')
// body   
// validationResult -  
const { body, validationResult } = require('express-validator')
const cors = require('cors')

const app = express()
const PORT = process.env.PORT || 5000

app.use(cors())
app.use(express.json())
app.use(express.urlencoded({ extended: false }))

// 
const validators = [
  body('name').trim().notEmpty().isAlpha('ru-RU').escape(),
  body('email').normalizeEmail().isEmail(),
  //  
  body('password').custom((value) => {
    const regex = /^[A-Z0-9_-]{8,12}$/i

    if (!regex.test(value)) throw new Error('   ')

    return true
  })
]

//     middleware
app.post('/server', validators, (req, res) => {
  //       
  const { errors } = validationResult(req)

  console.log(errors)

  //       
  if (errors.length) {
    res.status(400).json(' ')
  } else {
    res.status(201).json('  ')
  }
})

app.listen(PORT, () => {
  console.log(` . : ${PORT}`)
})

      
      





A complete list of available validators can be found here .



Let's add a couple of scripts to package.json - "server" to start the server and "dev" to start the servers simultaneously:



"scripts": {
  "start": "react-scripts start",
  "build": "react-scripts build",
  "server": "nodemon server",
  "dev": "concurrently \"yarn server\" \"yarn start\""
}

      
      





Executing yarn dev and testing the form submission:











Great. It seems we have succeeded.



We've covered a very simple client-server form validation. At the same time, more complex options only involve an increase in the number of validators, the general principles remain the same. It is also worth noting that client-side form validation can be done using HTML ( GitHub , CodeSandbox ).



Thank you for your attention and have a nice day.



All Articles