Skip to content

Instantly share code, notes, and snippets.

@makarovas
Created February 21, 2019 14:30
Show Gist options
  • Save makarovas/93776f9519c1b6f6f02356a1866f8179 to your computer and use it in GitHub Desktop.
Save makarovas/93776f9519c1b6f6f02356a1866f8179 to your computer and use it in GitHub Desktop.
Async form validation with react hooks
// https://codesandbox.io/s/x964kxp2vo?from-embed
import React, { Component, useState, useEffect, useRef } from "react";
export const useField = (
name,
form,
{ defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {}
) => {
let [value, setValue] = useState(defaultValue);
let [errors, setErrors] = useState([]);
let [pristine, setPristine] = useState(true);
let [validating, setValidating] = useState(false);
let validateCounter = useRef(0);
const validate = async () => {
let validateIteration = ++validateCounter.current;
setValidating(true);
let formData = form.getFormData();
let errorMessages = await Promise.all(
validations.map(validation => validation(formData, name))
);
errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
if (validateIteration === validateCounter.current) {
// this is the most recent invocation
setErrors(errorMessages);
setValidating(false);
}
let fieldValid = errorMessages.length === 0;
return fieldValid;
};
useEffect(
() => {
if (pristine) return; // Avoid validate on mount
form.validateFields(fieldsToValidateOnChange);
},
[value]
);
let field = {
name,
value,
errors,
setErrors,
pristine,
onChange: e => {
if (pristine) {
setPristine(false);
}
setValue(e.target.value);
},
validate,
validating
};
form.addField(field);
return field;
};
export const useForm = ({ onSubmit }) => {
let [submitted, setSubmitted] = useState(false);
let [submitting, setSubmitting] = useState(false);
let fields = [];
const validateFields = async fieldNames => {
let fieldsToValidate;
if (fieldNames instanceof Array) {
fieldsToValidate = fields.filter(field =>
fieldNames.includes(field.name)
);
} else {
//if fieldNames not provided, validate all fields
fieldsToValidate = fields;
}
let fieldsValid = await Promise.all(
fieldsToValidate.map(field => field.validate())
);
let formValid = fieldsValid.every(isValid => isValid === true);
return formValid;
};
const getFormData = () => {
return fields.reduce((formData, f) => {
formData[f.name] = f.value;
return formData;
}, {});
};
return {
onSubmit: async e => {
e.preventDefault();
setSubmitting(true);
setSubmitted(true); // User has attempted to submit form at least once
let formValid = await validateFields();
let returnVal = await onSubmit(getFormData(), formValid);
setSubmitting(false);
return returnVal;
},
isValid: () => fields.every(f => f.errors.length === 0),
addField: field => fields.push(field),
getFormData,
validateFields,
submitted,
submitting
};
};
export const useField = (
name,
form,
{ defaultValue, validations = [] } = {}
) => {
let [value, setValue] = useState(defaultValue);
let [errors, setErrors] = useState([]);
const validate = async () => {
let formData = form.getFormData();
let errorMessages = await Promise.all(
validations.map(validation => validation(formData, name))
);
errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
setErrors(errorMessages);
let fieldValid = errorMessages.length === 0;
return fieldValid;
};
useEffect(
() => {
form.validateFields(); // Validate fields when value changes
},
[value]
);
let field = {
name,
value,
errors,
validate,
setErrors,
onChange: e => {
setValue(e.target.value);
}
};
// Register field with the form
form.addField(field);
return field;
};
export const useForm = ({ onSubmit }) => {
let fields = [];
const getFormData = () => {
// Get an object containing raw form data
return fields.reduce((formData, field) => {
formData[field.name] = field.value;
return formData;
}, {});
};
const validateFields = async () => {
let fieldsToValidate = fields;
let fieldsValid = await Promise.all(
fieldsToValidate.map(field => field.validate())
);
let formValid = fieldsValid.every(isValid => isValid === true);
return formValid;
};
return {
onSubmit: async e => {
e.preventDefault(); // Prevent default form submission
let formValid = await validateFields();
return onSubmit(getFormData(), formValid);
},
addField: field => fields.push(field),
getFormData,
validateFields
};
};
const Field = ({
label,
name,
value,
onChange,
errors,
setErrors,
validate,
...other
}) => {
let showErrors = !!errors.length;
return (
<FormControl className="field" error={showErrors}>
<InputLabel htmlFor={name}>{label}</InputLabel>
<Input
id={name}
value={value}
onChange={onChange}
onBlur={validate}
{...other}
/>
<FormHelperText component="div">
{showErrors &&
errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
</FormHelperText>
</FormControl>
);
};
const App = props => {
const form = useForm({
onSubmit: async formData => {
window.alert("Account created!");
}
});
const usernameField = useField("username", form, {
defaultValue: "",
validations: [
async formData => {
await timeout(2000);
return formData.username.length < 6 && "Username already exists";
}
]
});
const passwordField = useField("password", form, {
defaultValue: "",
validations: [
formData =>
formData.password.length < 6 && "Password must be at least 6 characters"
]
});
const confirmPasswordField = useField("confirmPassword", form, {
defaultValue: "",
validations: [
formData =>
formData.password !== formData.confirmPassword &&
"Passwords do not match"
]
});
return (
<div id="form-container">
<form onSubmit={form.onSubmit}>
<Field {...usernameField} label="Username" />
<Field {...passwordField} label="Password" type="password" />
<Field {...confirmPasswordField} label="Confirm Password" type="password" />
<Button type="submit">Submit</Button>
</form>
</div>
);
};
export const useField = (
name,
form,
{ defaultValue, validations = [], fieldsToValidateOnChange = [name] } = {}
) => {
let [value, setValue] = useState(defaultValue);
let [errors, setErrors] = useState([]);
let [pristine, setPristine] = useState(true);
let [validating, setValidating] = useState(false);
let validateCounter = useRef(0);
const validate = async () => {
let validateIteration = ++validateCounter.current;
setValidating(true);
let formData = form.getFormData();
let errorMessages = await Promise.all(
validations.map(validation => validation(formData, name))
);
errorMessages = errorMessages.filter(errorMsg => !!errorMsg);
if (validateIteration === validateCounter.current) {
// this is the most recent invocation
setErrors(errorMessages);
setValidating(false);
}
let fieldValid = errorMessages.length === 0;
return fieldValid;
};
useEffect(
() => {
if (pristine) return; // Avoid validate on mount
form.validateFields(fieldsToValidateOnChange);
},
[value]
);
let field = {
name,
value,
errors,
setErrors,
pristine,
onChange: e => {
if (pristine) {
setPristine(false);
}
setValue(e.target.value);
},
validate,
validating
};
form.addField(field);
return field;
};
export const useForm = ({ onSubmit }) => {
let [submitted, setSubmitted] = useState(false);
let [submitting, setSubmitting] = useState(false);
let fields = [];
const validateFields = async fieldNames => {
let fieldsToValidate;
if (fieldNames instanceof Array) {
fieldsToValidate = fields.filter(field =>
fieldNames.includes(field.name)
);
} else {
//if fieldNames not provided, validate all fields
fieldsToValidate = fields;
}
let fieldsValid = await Promise.all(
fieldsToValidate.map(field => field.validate())
);
let formValid = fieldsValid.every(isValid => isValid === true);
return formValid;
};
const getFormData = () => {
return fields.reduce((formData, f) => {
formData[f.name] = f.value;
return formData;
}, {});
};
return {
onSubmit: async e => {
e.preventDefault();
setSubmitting(true);
setSubmitted(true); // User has attempted to submit form at least once
let formValid = await validateFields();
let returnVal = await onSubmit(getFormData(), formValid);
setSubmitting(false);
return returnVal;
},
isValid: () => fields.every(f => f.errors.length === 0),
addField: field => fields.push(field),
getFormData,
validateFields,
submitted,
submitting
};
};
const Field = ({
label,
name,
value,
onChange,
errors,
setErrors,
pristine,
validating,
validate,
formSubmitted,
...other
}) => {
let showErrors = (!pristine || formSubmitted) && !!errors.length;
return (
<FormControl className="field" error={showErrors}>
<InputLabel htmlFor={name}>{label}</InputLabel>
<Input
id={name}
value={value}
onChange={onChange}
onBlur={() => !pristine && validate()}
endAdornment={
<InputAdornment position="end">
{validating && <LoadingIcon className="rotate" />}
</InputAdornment>
}
{...other}
/>
<FormHelperText component="div">
{showErrors &&
errors.map(errorMsg => <div key={errorMsg}>{errorMsg}</div>)}
</FormHelperText>
</FormControl>
);
};
const App = props => {
const form = useForm({
onSubmit: async (formData, valid) => {
if (!valid) return;
await timeout(2000); // Simulate network time
if (formData.username.length < 10) {
//Simulate 400 response from server
usernameField.setErrors(["Make a longer username"]);
} else {
//Simulate 201 response from server
window.alert(
`form valid: ${valid}, form data: ${JSON.stringify(formData)}`
);
}
}
});
const usernameField = useField("username", form, {
defaultValue: "",
validations: [
async formData => {
await timeout(2000);
return formData.username.length < 6 && "Username already exists";
}
],
fieldsToValidateOnChange: []
});
const passwordField = useField("password", form, {
defaultValue: "",
validations: [
formData =>
formData.password.length < 6 && "Password must be at least 6 characters"
],
fieldsToValidateOnChange: ["password", "confirmPassword"]
});
const confirmPasswordField = useField("confirmPassword", form, {
defaultValue: "",
validations: [
formData =>
formData.password !== formData.confirmPassword &&
"Passwords do not match"
],
fieldsToValidateOnChange: ["password", "confirmPassword"]
});
let requiredFields = [usernameField, passwordField, confirmPasswordField];
return (
<div id="form-container">
<form onSubmit={form.onSubmit}>
<Field
{...usernameField}
formSubmitted={form.submitted}
label="Username"
/>
<Field
{...passwordField}
formSubmitted={form.submitted}
label="Password"
type="password"
/>
<Field
{...confirmPasswordField}
formSubmitted={form.submitted}
label="Confirm Password"
type="password"
/>
<Button
type="submit"
disabled={
!form.isValid() ||
form.submitting ||
requiredFields.some(f => f.pristine)
}
>
{form.submitting ? "Submitting" : "Submit"}
</Button>
</form>
</div>
);
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment