Form validation with Yup under React and Material-UI
Garry Xiao
Posted on May 2, 2020
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:
- Totally TypeScript
- Speed up development with depository support
- Custom hook
- 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
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
}
}
}
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>
)
First declear the validation schemas, initialize 'useFormValidator' and accept the returned methods for binding:
error={errors('password')}
helperText={texts('password')}
onChange={changeHandler}
onBlur={blurHandler}
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.
Posted on May 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.