DEV Community

Garry Xiao
Garry Xiao

Posted on

Form validation with Yup under React and Material-UI

In a real project, facing the form validation soon begin on frontend coding. After several rounds of refactoring, I completed it with 4 points in my project:

  1. Totally TypeScript
  2. Speed up development with depository support
  3. Custom hook
  4. Minimum refactoring for components

Chose Yup for validation schema definition, it is simple and easy to understand:
https://github.com/jquense/yup

npm install -S yup npm install -D @types/yup 
Enter fullscreen mode Exit fullscreen mode

React custom hook is a common function, with parameters for input and return necessary tool methods. useFormValidator as below is a custom hook and only rely on packages "react" and "yup", no relationship with the Material-UI framework:

import React from "react" import * as Yup from 'yup' /** * Form validator state field */ interface FormValidatorStateField { /** * Is error state */ error: boolean /** * state text */ text: string } /** * Form validator state fields */ interface FormValidatorStateFields { [key: string]: FormValidatorStateField } /** * Form validatior * @param schemas Initial validation schemas * @param milliseconds Merge change update interval */ export const useFormValidator = (schemas: Yup.ObjectSchema<object>, milliseconds: number = 200) => { // useState init const defaultState: FormValidatorStateFields = {} const [state, updateState] = React.useState<FormValidatorStateFields>(defaultState) // Change timeout seed let changeSeed = 0 // Change value handler const commitChange = (field: string, value: any) => { // Validate the field, then before catch, if catch before then, both will be triggered Yup.reach(schemas, field).validate(value).then(result => { commitResult(field, result) }).catch(result => { commitResult(field, result) }) } // Commit state result const commitResult = (field: string, result: any) => { let currentItem = state[field] if(result instanceof Yup.ValidationError) { // Error if(currentItem) { // First to avoid same result redraw if(currentItem.error && currentItem.text == result.message) return // Update state currentItem.error = true currentItem.text = result.message } else { // New item const newItem: FormValidatorStateField = { error: true, text: result.message } state[field] = newItem } } else { // Success and no result, just continue if(currentItem == null) return // Delete current state result delete state[field] } // Update state, for object update, need a clone const newState = {...state} updateState(newState) } // Clear timeout seed const clearSeed = () => { if(changeSeed > 0) clearTimeout(changeSeed) } // Delay change const delayChange = (field: string, value: any) => { clearSeed() changeSeed = setTimeout(() => { commitChange(field, value) }, milliseconds) } // Merge into the life cycle React.useEffect(() => { return () => { // clearTimeout before dispose the view clearSeed() } }, []) // Return methods for manipulation return { /** * Input or Textarea blur handler * @param event Focus event */ blurHandler: (event: React.FocusEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = event.currentTarget delayChange(name, value) }, /** * Input or Textarea change handler * @param event Change event */ changeHandler: (event: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => { const { name, value } = event.currentTarget delayChange(name, value) }, /** * Commit change */ commitChange: commitChange, /** * State error or not * @param field Field name */ errors: (field: string) => { return state[field]?.error }, /** * State text * @param field Field name */ texts: (field: string) => { return state[field]?.text }, /** * Validate form data * @param data form data, Object.fromEntries(new FormData(form)) */ validate: async (data: any) => { try { clearSeed() return await schemas.validate(data, { strict: true, abortEarly: false, stripUnknown: false }) } catch(e) { // Reset const newState: FormValidatorStateFields = {} // Iterate the error items if(e instanceof Yup.ValidationError) { for(let error of e.inner) { // Only show the first error of the field if(newState[error.path] == null) { // New item const newItem: FormValidatorStateField = { error: true, text: error.message } newState[error.path] = newItem } } } // Update state updateState(newState) } return null } } } 
Enter fullscreen mode Exit fullscreen mode

When use it in Materal-UI pages, for example a login page:

// Login component function Login() { // Form validator const { blurHandler, changeHandler, errors, texts, validate } = useFormValidator(validationSchemas) // Login action async function doLogin(event: React.FormEvent<HTMLFormElement>) { // Prevent default action event.preventDefault() // Form JSON data let data = await validate(Object.fromEntries(new FormData(event.currentTarget))) if(data == null) return // Local data format // Parase as model const model = data as LoginModel } return ( <Container component="main" maxWidth="xs"> <CssBaseline /> <img src={window.location.origin + '/logo.jpg'} alt="Logo" className={classes.logo}/> <div className={classes.paper}> <Avatar className={classes.avatar}> <LockOutlined /> </Avatar> <Typography component="h1" variant="h5"> Sign in </Typography> <form className={classes.form} onSubmit={doLogin} noValidate> <TextField variant="outlined" margin="normal" required fullWidth id="id" label="Id or Email" name="id" error={errors('id')} helperText={texts('id')} onChange={changeHandler} onBlur={blurHandler} autoComplete="email" autoFocus /> <TextField variant="outlined" margin="normal" type="password" required fullWidth name="password" error={errors('password')} helperText={texts('password')} onChange={changeHandler} onBlur={blurHandler} label="Password" id="password" autoComplete="current-password" /> <FormControlLabel control={<Checkbox name="save" value="true" color="primary" />} label="Remember me" /> <Button type="submit" fullWidth variant="contained" color="primary" className={classes.submit} > Sign In </Button> </form> </div> </Container> ) 
Enter fullscreen mode Exit fullscreen mode

First declear the validation schemas, initialize 'useFormValidator' and accept the returned methods for binding:

 error={errors('password')} helperText={texts('password')} onChange={changeHandler} onBlur={blurHandler} 
Enter fullscreen mode Exit fullscreen mode

Through bindings only to current components to indicate any validation errors occur. No refactoring or extending for current components. That's the key feature of the task I enjoyed.

Top comments (2)

Collapse
 
pablomarch profile image
Pablo Marchena • Edited

This is the ugliest article I have seen in a long time, no code format, no explanations, long files with few comments...

Collapse
 
garryxiao profile image
Garry Xiao

It's a rush for a reference/guide during my interview. Sorry about that. I am working on a repository of the source codes, maybe later will much helpful.